JVM-垃圾回收(二)

接着上次 JVM 中 GC 机制的总结,这次主要复习一下垃圾收集的常用算法和 Minor GC、Full GC 相关的一些知识点。

一、垃圾收集算法

1.1 标记 - 清除(Mark-Sweep)

算法分成 “标记”、“清除” 两个阶段:首先标记出所有需要回收的对象(两次标记),在标记完成后统一回收所有被标记的对象。如下图所示:

标记-清除算法的不足主要有以下两点:

  • 空间问题,会产生大量不连续的内存碎片,导致无法给大对象分配内存。
  • 效率问题,因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。

1.2 标记 - 整理(Mark-Compact)

此算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。具体示意图如下所示:

1.3 复制(Copying)

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。主要不足是只使用了内存的一半。

复制算法过程:

1.4 分代收集

JVM 采用分代收集(Generational Collection)算法,此算法相较于前几种没有什么新的特征,主要思想为:根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法:

  • 新生代使用复制算法 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代使用标记 - 清理 或者 标记 - 整理 算法 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 - 清除” 或 “标记 - 整理” 算法来进行回收。

二、Minor GC 和 Full GC

2.1 Minor GC

发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Minor GC 会使用复制收集算法进行垃圾回收,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

2.2 Full GC

发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

2.3 Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

2.3.1 调用 System.gc()

此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存,可通过 -XX:+ DisableExplicitGC 虚拟机参数来禁止 RMI 调用 System.gc()

2.3.2 老年代空间不足

老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 虚拟机参数调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

2.3.3 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

2.3.4 JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS 垃圾收集器的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS 垃圾收集器。

在 JDK 1.8 中用元空间替换了永久代作为方法区的实现,元空间是本地内存,因此减少了一种 Full GC 触发的可能性。

2.3.5 Concurrent Mode Failure

使用 CMS 垃圾收集器执行的过程中,同时有对象要放入老年代,而此时老年代空间不足(有时候 “空间不足” 是指 CMS GC 当前的浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。