JVM垃圾回收

39次阅读

共计 2800 个字符,预计需要花费 7 分钟才能阅读完成。

带着三个问题看垃圾回收 
1. 回收谁
2. 什么时候回收
3. 怎么回收

1. 回收谁

引用计数法:给对象中添加一个引用计数器,每当有对象引用它时,计数器就加 1,引用失效就减 1,计数器到 0 的时候代表不能使用该对象,不能解决循环引用的问题

可达性分析:通过 GCRoots 做为起点,从这个起点向下搜索,当一个对象到 GCRoots 没有任何引用链相连的时候,这个对象是不可用的,可以回收。虚拟机栈(栈帧变量表中引用的对象),方法区(类的静态属性引用的对象,常量引用的对象),本地方法中 JNI(Native 引用的对象)

“ 食之无味,弃之可惜 ”
强引用(认可 OOM,也不会回收)
软引用(系统 OOM 之前,这些对象被回收)
弱引用(无论内存够不够,都会回收)
虚引用(只会收到回收通知)

“ 最后一刻挣扎 ”
一个对象的死亡,至少要被标记两次,第一次看有没有必要执行该对象中的 finalize 方法,如果该方法被调用过或者对象没有覆盖整个方法,就没有必要执行 finalize。如果执行了 finalize,可以在方法里面自救,自救方案是与引用链上任何一个对象关联即可。不建议使用

方法区的回收
回收效率低,回收严谨。只有满足以下三点才会回收
1. 该类的所有实例都被回收,堆中不存在任何该类的实例
2. 加载该类的 ClassLoader 已被回收
3. 该类的 java.lang.Class 对象没有任何地方被应用,无法通过反射来访问该类

2. 什么时候回收

应用线程空闲时
内存满的时候

3. 怎么回收

标记清除算法

先标记要回收的对象,在统一清除。缺陷是会产生大量不连续的内存碎片,在分配大对象时,不得不提前触发另一次垃圾收集动作

复制算法

将内存分 AB 两块,每次只用一块,A 的内存用完了,回收的时候就将 A 还存活的对象放在 B 上,然后统一清理 A。缺陷是对象多的时候浪费复制时间,对内存的开销也比较大。

标记整理算法

标记出所有要回收的对象,标记完成后,将存活的对象移动一端,然后清理掉端边界以外的内存。

分代收集算法
根据新生代和老年代的特点,使用分代收集算法

因为新生代朝生夕死,所以用复制算法,仅需要复制少量对象。
老年代存活率高,对象多,没有额外空间进行分配,就使用标记 - 整理算法。可以自由搭配很多种,不过大致的类型就是以下几种。

串行收集器(Serial)

-XX:+SerialGC
单线程收集器,这里的单线程不是指垃圾回收的线程只有一个,而是相对于应用程序来讲,在回收垃圾的时候要暂停应用程序(STW)
在内存不足时,串行 GC 设置停顿标识,当所有线程到达安全点后,应用程序暂停,开始垃圾收集。适合堆内存不高且单核的 cpu 使用。

并行收集器(ParNew,Parallel Scavenge)

-XX:+UseParNewGC
ParNew 是 Serial 收集器的多线程版本,搭配老年代 CMS 首选。适合多核 cpu。ParallelScavenge 更关注吞吐量,同样需要 STW,适合和前台交互少的系统,后台处理任务量大的

并发收集器(CMS)
更关注低延迟的收集器,分为以下四个阶段,适合内存大,多核 cpu。缺陷:消耗内存过的大,容易引起 fullGC。有碎片,为防止 fullGC,默认开启碎片整理参数

初始标记:以 STW 的方式标记所有根对象,很快
并发标记,与程序并发执行,标记出所有根路径的可达路径
重新标记,以 STW 标记有可能在这期间错过的,同样很快
并发清除,将不可达对象并发回收

G1 收集器
引入了 Region 概念,和 CMS 比较像,只不过有 Region 的优势

观看 GC 日志

33.125 代表虚拟机启动到现在,经过了多少秒
Full GC 和 GC 代表停顿类型,不是为了区分新生代和老年代的,如果有 Full,代表是以 SWT 触发的垃圾收集
DefNew,Tenured,Perm 才是区域,发生在什么区域上的
3324K – > 152K(3712K):GC 前该内存区域已使用的容量 -> GC 后该内存已使用的容量(总容量)

内存分配与回收策略

  • 对象优先在 Eden 分配,如果说 Eden 内存空间不足,就会发生 Minor GC
  • 大对象直接进入老年代,大对象需要大量连续内存空间的 Java 对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。-XX:PretenureSizeThreshold 参数,大于这个数量直接在老年代分配,缺省为 0,表示绝不会直接分配在老年代。
  • 长期存活的对象将进入老年代,默认 15 岁,-XX:MaxTenuringThreshold 调整
  • 动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄
  • 空间分配担保:新生代中有大量的对象存活,survivor 空间不够,当出现大量对象在 MinorGC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接进入老年代. 只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行 Minor GC,否则 FullGC。

内存泄漏和内存溢出
内存泄漏是该释放的对象没有得到释放
内存溢出是撑爆了内存,对象太多了

JDK 为我们提供的工具

  • jps

列出当前机器上正在运行的虚拟机进程
-p : 仅仅显示 VM 标示,不显示 jar,class, main 参数等信息.
-m: 输出主函数传入的参数. 下的 hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整 package 名称或 jar 完整名称.
-v: 列出 jvm 参数, -Xms20m -Xmx50m 是启动程序指定的 jvm 参数

  • jstat

是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
假设需要每 250 毫秒查询一次进程 2764 垃圾收集状况,一共查询 20 次,那命令应当是:jstat-gc 2764 250 20
常用参数:
-class (类加载器)
-compiler (JIT)
-gc (GC 堆状态)
-gccapacity (各区大小)
-gccause (最近一次 GC 统计和原因)
-gcnew (新区统计)
-gcnewcapacity (新区大小)
-gcold (老区统计)
-gcoldcapacity (老区大小)
-gcpermcapacity (永久区大小)
-gcutil (GC 统计汇总)
-printcompilation (HotSpot 编译统计)

  • jmap
  • jstack
  • ………
正文完
 0

JVM垃圾回收

41次阅读

共计 4881 个字符,预计需要花费 13 分钟才能阅读完成。

1. 要回收的内存区域

Java 虚拟机的内存模型分为五个部分,分别是:程序计数器、Java 虚拟机栈、本地方法栈、堆、方法区。程序计数器、Java 虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁,因此这三个区域不需要垃圾回收。
堆和方法区所有线程共享,并且都在 JVM 启动时创建,一直得运行到 JVM 停止时。因此它们没办法根据线程的创建而创建、线程的结束而释放。堆中存放 JVM 运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定。因此,堆和方法区的内存回收具有不确定性,因此垃圾回收器要负责这两个区域的垃圾回收。

2. 堆内存回收

2.1 堆内存回收判定方式

在对堆进行对象回收之前,首先要判断哪些是无效对象。一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收。一般有两种判别方式:

  • 引用计数法

      每个对象都有一个计数器,当这个对象被一个变量或另一个对象引用一次,该计数器加一;若该引用失效则计数器减一。当计数器为 0 时,就认为该对象是无效对象。

  • 可达性分析法

      所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。GC Roots 是指:
      1.Java 虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象)
      2. 本地方法栈所引用的对象
      3. 方法区中静态属性引用的对象
      4. 方法区中常量所引用的对象

PS:注意!GC Roots 并不包括堆中对象所引用的对象!这样就不会出现循环引用。
两者对比:,
引用计数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题。
因此,目前主流语言均使用可达性分析方法来判断对象是否有效。

2.2 堆内存回收过程

  1. 判断该对象是否覆盖了 finalize()方法

    1. 若已覆盖该方法,并该对象的 finalize()方法还没有被执行过,那么就会将 finalize()扔到 F -Queue 队列中;
    2. 若未覆盖该方法,则直接释放对象内存。
  2. 执行 F -Queue 队列中的 finalize()方法: 虚拟机会以较低的优先级执行这些 finalize()方法们,也不会确保所有的 finalize()方法都会执行结束。如果 finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除。
  3. 对象重生或死亡: 如果在执行 finalize()方法时,将 this 赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。

注意:
强烈不建议使用 finalize()函数进行任何操作!如果需要释放资源,请使用 try-finally
因为 finalize()不确定性大,开销大,无法保证顺利执行。

3. 方法区内存回收

由于方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。
方法区中主要清除两种垃圾:

  1. 废弃 常量
  2. 废弃的

3.1 如何判断废弃常量

清除废弃的常量和清除对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。

3.2 如何判断废弃的类

清除废弃类的条件较为苛刻:

  1. 该类的所有对象都已被清除
  2. 该类的 java.lang.Class 对象没有被任何对象或变量引用。只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。
  3. 加载该类的 ClassLoader 已经被回收

4. 垃圾回收算法

4.1 标记 - 清除算法

“标记 - 清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

“标记 - 清除”算法的不足主要有两个:
效率问题:标记和清除这两个过程的效率都不高
空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次

4.2 复制算法(新生代回收算法)

“复制”算法是为了解决“标记 - 清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
现在的商用虚拟机 (包括 HotSpot) 都是采用这种收集算法来回收新生代
新生代中 98% 的对象都是 ” 朝生夕死 ” 的,所以并不需要按照 1 : 1 的比例来划分内存空间,而是将内存 (新生代内存) 分为一块较大的 Eden(伊甸园)空间和两块较小的 Survivor(幸存者)空间,每次使用 Eden 和其中一块 Survivor(两个 Survivor 区域一个称为 From 区,另一个称为 To 区域)。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
当 Survivor 空间不够用时,需要依赖其他内存 (老年代) 进行分配担保。
HotSpot 默认 Eden 与 Survivor 的大小比例是 8 : 1,也就是说 Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的 90%, 而剩下的 10% 用来存放回收后存活的对象。
HotSpot 实现的复制算法流程如下

  1. 当 Eden 区满的时候,会触发第一次 Minor gc,把还活着的对象拷贝到 Survivor From 区;当 Eden 区再次出发 Minor gc 的时候,会扫描 Eden 区和 From 区,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域,并将 Eden 区和 From 区清空。
  2. 当后续 Eden 区又发生 Minor gc 的时候,会对 Eden 区和 To 区进行垃圾回收,存活的对象复制到 From 区,并将 Eden 区和 To 区清空
  3. 部分对象会在 From 区域和 To 区域中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还存活,就存入老年代。

4.3 标记整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为“标记 - 整理算法”。标记过程仍与“标记 - 清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

4.4 分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法。

5. 垃圾收集器

垃圾收集器分为新生代垃圾收集器,老年代垃圾收集器,通用垃圾收集器。重点掌握 CMS 垃圾收集器和 G1 垃圾收集器。

1. CMS 垃圾收集器

CMS 收集器是一款追求停顿时间的老年代收集器,它在垃圾收集时使得用户线程和 GC 线程并行执行,因此在垃圾收集过程中用户也不会感受到明显的卡顿。但用户线程和 GC 线程之间不停地切换会有额外的开销,因此垃圾回收总时间就会被延长。
垃圾回收过程

  • 1. 初始标记

停止一切用户线程,仅使用一条初始标记线程对所有与 GC ROOTS 直接关联的对象进行标记。速度很快。

  • 2. 并发标记

使用多条并发标记线程并行执行,并与用户线程并发执行。此过程进行可达性分析,标记出所有废弃的对象。速度很慢。

  • 3. 重新标记

停止一切用户线程,并使用多条重新标记线程并行执行,将刚才并发标记过程中新出现的废弃对象标记出来。这个过程的运行时间介于初始标记和并发标记之间。

  • 4. 并发清除

只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象。这个过程非常耗时。
CMS 缺点

  • 1. 吞吐量低

由于 CMS 在垃圾收集过程使用用户线程和 GC 线程并行执行,从而线程切换会有额外开销,因此 CPU 吞吐量就不如在垃圾收集过程中停止一切用户线程的方式来的高。

  • 2. 无法处理浮动垃圾,导致频繁 Full GC

由于垃圾清除过程中,用户线程和 GC 线程并发执行,也就是用户线程仍在执行,那么在执行过程中会产生垃圾,这些垃圾称为“浮动垃圾”。
如果 CMS 在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足时,就需要再次发起 Full GC,而此时 CMS 正在进行清除工作,因此此时只能由 Serial Old 临时对老年代进行一次 Full GC。

  • 3. 使用“标记 - 清除”算法产生碎片空间

由于 CMS 使用了“标记 - 清除”算法,因此清除之后会产生大量的碎片空间,不利于空间利用率。不过 CMS 提供了应对策略:
开启 -XX:+UseCMSCompactAtFullCollection
开启该参数后,每次 FullGC 完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块儿。但每次都整理效率不高,因此提供了以下参数。
设置参数 -XX:CMSFullGCsBeforeCompaction
本参数告诉 CMS,经过了 N 次 Full GC 过后再进行一次内存整理。

2.G1 垃圾收集器

G1 的特点
1. 追求停顿时间
2. 多线程 GC
3. 面向服务端应用
4. 标记 - 整理和复制算法合并。与 CMS 的“标记 – 清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
5. 可对整个堆进行垃圾回收
6. 可预测停顿时间:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,
G1 的内存模型
G1 垃圾收集器没有新生代和老年代的概念了,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中的垃圾数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
Remembered Set
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
当然不是,每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在 GC ROOTs 中再加上 Remembered Set 即可防止对所有堆内存的遍历。
G1 垃圾收集过程
1. 初始标记:仅标记与 GC ROOTS 直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快。
2. 并发标记:进行全面的可达性分析,找出存活的对象,开启一条并发标记线程与用户线程并行执行。这个过程比较长。
3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这一阶段需要停顿线程,但是可并行执行。
4. 筛选回收: 首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。

6.Java 中的引用类型

Java 中根据生命周期的长短,将引用分为 4 类。

1. 强引用

通过关键字 new 创建的对象所关联的引用就是强引用。只要强引用存在,该对象永远也不会被回收。

2. 软引用

只有当堆即将发生 OOM 异常时,JVM 才会回收软引用所指向的对象。
软引用通过 SoftReference 类实现。
软引用的生命周期比强引用短一些。

3. 弱引用

只要垃圾收集器运行,软引用所指向的对象就会被回收。
弱引用通过 WeakReference 类实现。
弱引用的生命周期比软引用短。

4. 虚引用

虚引用也叫幽灵引用,它和没有引用没有区别,无法通过虚引用访问对象的任何属性或函数。
一个对象关联虚引用唯一的作用就是在该对象被垃圾收集器回收之前会受到一条系统通知。
虚引用通过 PhantomReference 类来实现。

正文完
 0