JVM GC知识回顾

这两天刚好有朋友问到我面试中GC相关问题应该怎么答,作为java面试中热门问题,其实没有什么标准回答。这篇文章结合自己之前的总结,对GC相关知识做一个回顾。

1.分代收集

当前主流VM垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代、老年代、永久代. 这样就可以根据各年代特点分别采用最适当的GC算法:

  • 在新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集.
  • 在老年代: 因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记—清理”或“标记—整理”算法来进行回收,不必进行内存复制,且直接腾出空闲内存.
JVM GC知识回顾_第1张图片
分代收集

1.1 新生代-复制算法

该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.

JVM GC知识回顾_第2张图片
复制算法

这使得每次只对其中一块内存进行回收, 分配也就不用考虑内存碎片等复杂情况, 实现简单且运行高效.


JVM GC知识回顾_第3张图片
复制算法流程

现代商用VM的新生代均采用复制算法, 但由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代.

1.2 老年代-标记清除算法

该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.

JVM GC知识回顾_第4张图片
标记清除算法

该算法会有以下两个问题:

  1. 效率问题: 标记和清除过程的效率都不高;
  2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.
JVM GC知识回顾_第5张图片
标记清除算法流程

1.3 老年代-标记整理算法

标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.

JVM GC知识回顾_第6张图片
标记整理算法

1.4 永久代-方法区回收

在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量和无用的类. 回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:

  1. 该类所有的实例都已经被回收, Java堆中不存在该类的任何实例;
  2. 该类对应的Class对象没有在任何地方被引用(也就是在任何地方都无法通过反射访问该类的方法);
  3. 加载该类的ClassLoader已经被回收.

但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此++在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能,以保证方法区不会溢出++.

补充:空间分配担保
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).

2.分区收集

上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分, 而分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间.
在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿.

2.0 分区收集- G1收集器

G1(Garbage-First)是一款面向服务端应用的收集器, 主要目标用于配备多颗CPU的服务器治理大内存. G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
-XX:+UseG1GC 启用G1收集器.

与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.

JVM GC知识回顾_第7张图片
G1收集器

每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.

2.1 新生代收集

JVM GC知识回顾_第8张图片
新生代收集

G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.

JVM GC知识回顾_第9张图片
image

G1的新生代收集特点如下:

  • 一整块堆内存被分为多个Regions.
  • 存活对象被拷贝到新的Survivor区或老年代.
  • 年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域尺寸.
  • Young GCs会有STW事件, 进行时所有应用程序线程都会被暂停.
  • 多线程并发GC.

2.2 老年代收集

G1老年代GC会执行以下阶段:

注: 一下有些阶段也是年轻代垃圾收集的一部分.

Index Phase Description
1 初始标记 (Initial Mark: Stop the World Event) 在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
2 扫描根区域 (Root Region Scanning: 与应用程序并发执行) 扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完.
3 并发标记 (Concurrent Marking : 与应用程序并发执行) 在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断.
4 重新标记 (Remark : Stop the World Event) 完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).
5 清理 (Cleanup : Stop the World Event and Concurrent) 见下 5-1、2、3
5-1 (Stop the world) 在含有存活对象和完全空闲的区域上进行统计
5-2 (Stop the world) 擦除Remembered Sets.
5-3 (Concurrent) 重置空regions并将他们返还给空闲列表(free list)
(*) Copying/Cleanup (Stop the World Event) 选择”活跃度”最低的区域(这些区域可以最快的完成回收). 拷贝/转移存活的对象到新的尚未使用的regions. 该阶段会被记录在gc-log内(只发生年轻代[GC pause (young)], 与老年代一起执行则被记录为[GC Pause (mixed)].

详细步骤可参考 Oracle官方文档-The G1 Garbage Collector Step by Step.

G1老年代GC特点如下:

  • 并发标记阶段(index 3)
    1.在与应用程序并发执行的过程中会计算活跃度信息.
    2.这些活跃度信息标识出那些regions最适合在STW期间回收(which regions will be best to reclaim during an evacuation pause).
    3.不像CMS有清理阶段.

  • 再次标记阶段(index 4)
    1.使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.
    2.空region直接被回收.

  • 拷贝/清理阶段(Copying/Cleanup - Phase)
    1.年轻代与老年代同时回收.
    2.老年代内存回收会基于他的活跃度信息.

你可能感兴趣的:(JVM GC知识回顾)