JVM GC收集器总览与比较

304 阅读9分钟

必备知识点: 1. 可达性分析:JVM内存管理系统通过可达性分析算法来判定对象是否存活。基本思路是从GC Roots节点根据引用关系向下扫描,扫描所有可达的对象。虚拟机栈区本地变量引用的对象、常量引用的对象、类属性引用的对象、本地方法栈引用的对象、基本数据类型对应的Class对象、系统类加载器等可作为为GC Roots节点。 2. 安全点:收集器回收内存过程中,有步骤不能与用户线程并行时,需要用户线程在安全点暂停,然后才可以继续回收工作。 3. 标记:内存释放前需要将可回收与不可回收的对象标记出来,三色标记法。 4. 内存回收:回收方法有三种,分别是清除、整理、复制。 - 清除只是将无用对象空间释放,有产生大量内存碎片。 - 整理会将有用的对象有序移到一处空闲空间,不会产生内存碎片。 - 复制是将有用对象复制到另外一处空间,然后将之前使用的空间全部释放,不会产生内存碎片,逻辑较移动简单,但是需要更多的冗余内存。 5. 内存分代:有内存分代的情况下,一般有新生代、老年代。 6. 并发标记中,引用关系发生变动,确保标记正确的方法:增量重新标记,快照标记。 7. 跨代引用:老年代对象引用新生代对象,新生代回收时,需要将引用了新生代对象老年代对象也作为GC ROOT节点。

Serial收集器

分代:是
管理代区:新生代
与用户进程运行关系:串行
GC线程:单线程
关注延时or吞吐:无
回收步骤:等待所有用户线程在安全点暂停——>标记——>复制方式回收——>用户线程继续。
优点:消耗小,适合在低配置客户端,或者研发机使用。
缺点:不能有效发挥多核硬件的优势,与用户线程串行而Stop The World。

Serial Old收集器

分代:是
管理代区:老年代
与用户进程运行关系:串行
GC线程:单线程
关注延时or吞吐:无
回收步骤:用户线程安全点暂停——>标记——>整理方式回收——>用户线程继续。
优点:同Serial收集器。
缺点:同Serial收集器。

ParNew收集器

分代:是
管理代区:新生代
与用户进程运行关系:串行
GC线程:多线程
关注延时or吞吐:无
回收步骤:用户线程安全点暂停——>标记——>复制方式回收——>用户线程继续。
优点:Serial收集器升级版,多核机器上使用可以利用多核特性。
缺点:与用户线程完全是串行关系,还是会Stop The Word。

Parallel Scavenge收集器

分代:是
管理代区:新生代
与用户进程运行关系:串行
GC线程:多线程
关注延时or吞吐:吞吐(设定最大停顿时间,或吞吐量,GC自适应调整内存参数,如新生代大小、老年代大小,新生代越大,则回收频率越低,总停顿时间越少,吞吐量越大)
回收步骤:用户线程安全点暂停——>标记——>复制方式回收——>用户线程继续。
优点:参数设置方便,可以自适应;在需要更高的吞吐,而对停顿时间容忍度高的情况下,可以大展身手,提供高吞吐量。
缺点:也会Stop The Word,在低延时领域不擅长。

CMS收集器

分代:是
管理代区:老年代
与用户进程运行关系:并发标记(与用户线程并发),并发清除(与用户线程并发)
GC线程:多线程
关注延时or吞吐:延迟
回收步骤:用户线程安全点暂停—>初始标记——>并发标记——>重新标记(使用增量更新)——>并发清除。
优点:最耗时的标记过程可以跟用户线程并发,大大降低了GC停顿时间。
缺点:
- 回收方式是清除,会产生内存碎片,积累下来,可能由于内存的分散而不够给较大对象分配空间,而提前触发Full GC。
- 由于与用户线程并发,所以不能在老年代满了以后再进行收集,需要预留一部分空间,而在并发收集过程中,如果预留的空间无法满足
用户线程分配新对象的需求,会转而使用Serial Old,导致停顿时间更长。
- 并发收集过程中处理不了浮动垃圾。

Parallel Old收集器

分代:是
管理代区:老年代
与用户进程运行关系:串行
GC线程:多线程
关注延时or吞吐:吞吐(此收集器出现前,由于代码体系不同,Parallel Scavenge只能与Serial Old配合,没有高效的老年代收集器可配合,)
回收步骤:用户线程安全点暂停——>标记——>整理方式回收——>用户线程继续。
优点:参数设置方便,可以自适应;在需要更高的吞吐,而对停顿时间容忍度高的情况下,可以大展身手,提供高吞吐量。
缺点:也会Stop The Word,在低延时领域不擅长。

G1收集器

分代:是
管理代区:全部代区
与用户进程运行关系:并发标记,串行整理
GC线程:多线程
关注延时or吞吐:延时(将内存空间划分为大小相同的独立区域,区域可用作新生代,也可以用作老年代,回收内存时,在标记结果上分析哪些区域回收性价比高,结合延时参数的设置,增量回收部分区域)
回收步骤:用户线程安全点暂停——>初始标记——>并发标记——>用户线程安全点暂停——>最终标记(使用原始快照)——>筛选回收(区域回收会将一个区域中的存活对象复制到另外一个区域,从整体内存看是整理)——>用户线程继续。
优点:
	- 增量回收使得收集器运行更加可控,开启了收集器新的里程碑。
    - 筛选回收使得停顿时间可控。
缺点:
	- 跨区域引用维护了双边关系,收集器消耗内存大
    - 内存回收速度如果赶不上内存分配速度,同样会触发Full GC。

Shenandoah收集器

分代:否
与用户进程运行关系:并发标记,并发整理
GC线程:多线程
关注延时or吞吐:延时(大体类似于G1,回收阶段可与用户线程并发)
回收步骤:用户线程安全点暂停——>初始标记——>并发标记——>用户线程安全点暂停——>最终标记(使用原始快照)——>并发清理(整个区域没有任何存活对象的直接整区域清理)——>并发回收(区域回收会将一个区域中的存活对象复制到另外一个区域,复制过程是与用户线程并发的,被移动对象访问与写的正确性是通过转发指针保证的)——>初始引用更新(GC线程集合点,确保所有GC线程完成负责的回收任务)——>并发引用更新(将引用旧址改为新值,也就是复制后存放的位置)——>用户线程安全点暂停——>最终引用更新(修正GC Roots中的引用)——>并发清理(引用更新完后,被复制对象的区域可以完全释放)。
优点:
	- 标记、回收主要耗时的过程都是与用户线程并发的,停顿时间很少。
    - 相比G1,使用连接矩阵方式来维护卡集,收集器自身运行对内存的占用更少
缺点:
	- 引用类对象的读写需要经过二次转发,类似于句柄,增加了对对象读写的代价,降低吞吐量
    - 没有分代,无法针对性回收

ZGC收集器

分代:否
与用户进程运行关系:并发标记,并发分配,并发重映射
GC线程:多线程
关注延时or吞吐:延时+吞吐,吞吐仅次于Parallel,且非常接近,甚至有些场景下优于Parallel
回收步骤:用户线程安全点暂停——>初始标记——>并发标记与上次GC的并发重映射——>用户线程安全点暂停——>最终标记(使用原始快照)——>并发预备重分配(计算哪些区域的对象要被复制到其它区域,要复制的区域后面会被清理)——>并发重分配(ZGC能超越Shenandoah的重要原因是没有使用指针转发,而是使用了染色指针,在引用中用部分位标记了引用地址是否有效,并有转发表记录了就对象到新对象的映射关系,第一次访问被移动的对象后,引用的地址就会修正为新地址,后面的访问都不需要二次寻址)。
优点:
	- 标记、回收过程都是与用户线程并发的,停顿时间很少。
    - 相比Shenandoah,并发回收过程,对象因复制而增加的访问成本很低
    - 相比G1,ZGC区域划分为小型、中型、大型,大型区域的对象是不会做重分配,不会进行复制的,整理更高效
    - 几近完美,停顿时间短,吞吐量高
缺点:
	- 没有分代,无法针对性回收
    - 对象分配速度太高时,会积累大量浮动垃圾

收集器比较合适的配合选项:

SerialSerial Old 适合单核环境
ParNew 与 CMS 适合多核小内存(4G以下,最大到6G,处于性能考虑,而不是能不能用)环境
Parallel Scavenge 与 Parallel Old 适合多核环境,对吞吐量要求高,但是能容忍较大停顿时间的场景
G1 适合多核,大内存环境
Parallel Scavenge 适合多核,大内存环境,响应性优于G1,吞吐量也不会逊色G1很多(JDK13中引用转发只针对数据类型为引用类型的对象,原生数据类型不再需要转发)
ZGC 适合多核,大内存环境,内核有自己管理的内存更佳(支持NUMA-Aware的内存分配),处于实验阶段