上一节主要学习了垃圾回收算法,包括标记-清除,标记-整理,标记-复制,分代回收算法,这一节主要学习垃圾回收算法的具体实现,也就是垃圾回收器。同一种垃圾回收算法,可以有不同的实现,也就对应着不同的垃圾回收器。
接下来我们主要学习一下Serial垃圾回收器,Parallel垃圾回收器,CMS垃圾回收器,G1垃圾回收器,ZGC垃圾回收器,Shenandoah垃圾回收器,Epsilon垃圾回收器。
Overview
首先我们先看一下垃圾回收器的整体历史
- Serial GC和Parallel GC都是一直存在的,CMS自java 14版本开始被删除。
- Java8包括java8以前的版本使用的默认GC都是Parallel GC,Java8以后的版本默认GC都是G1 GC。
- Epsilon GC自出生以来,都是实验室版本的。
- ZGC和Shenandoah GC自Java15以后转正。
Serial GC
- Serial垃圾回收器使用单线程进行垃圾回收,需要STW。
- 根据分代回收算法,Serial GC又分为Serial New垃圾回收器和Serial Old垃圾回收器。
- Serial New用于新生代的垃圾回收,采用标记-复制算法。
- Serial Old用于老年代的垃圾回收,采用标记-整理算法。
要启用此款收集器,只需要指定一个 JVM 启动参数即可,同时对年轻代和老年代生效:
-XX:+UseSerialGC
Serial GC不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。
Parallel GC
- Parallel垃圾回收器使用多线程进行垃圾回收,是Serial GC的多线程版本,同时也是需要STW的。
- Parallel垃圾回收器可以细分为Parallel Scavenge,ParNew,Parallel Old。
- Parallel Scavenge和ParNew都是用于新生代,采用的是标记-复制算法。
- Parallel Old用于老年代,采用的是标记-整理算法。
- Parallel Scavenge跟Parallel Old配合使用。可以使用
-XX:+UseParallelGC和-XX:+UseParallelOldGC来强制指定Parallel Scavenge作为新生代收集器,Parallel Old作为老年代GC - ParNew和CMS配合使用。可以
-XX:+UseConcMarkSweepGC或者-XX:+UseParNewGC来强制指定ParNew作为新生代收集器。
Parallel GC适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量:
- 在GC期间,所有CPU内核都在并行清理垃圾,所以总暂停时间更短;
- 在两次GC周期的间隔期,没有GC线程在运行,不会消耗任何系统资源。
CMS GC
前面提到的GC(Serail GC和Parallel GC)都会造成STW,分代回收算法让我们频繁做Young GC,少量做Full GC,但真的做Full GC时停顿时间还是非常大,那如何减少STW的时间,就成了后续垃圾收集器不断优化的点,人们最容易想到的就是并发。CMS中的CM指的是Concurrent Mark即是“并发标记”。而Shenandoah GC和ZGC又实现了“回收”的并发。
需要注意的是“并发”和“并行”在GC里的概念是不一样的,可以这么去区分:
- 并行:起多个线程一起处理,但对应用线程依旧是STW的
- 并发:GC线程处理GC任务的同时,应用线程依旧可以运行
如Parallel GC本质上就是“并行”而不是“并发”,GC过程还是 STW的。虽然仅一字之差,“并发”会带来非常多的问题,新的GC算法也用了许多解决方案,但这些方案都是有代价的。
目前CMS仅支持老年代的GC,ParNew作为新生代的GC与CMS配合使用。
CMS将整个垃圾回收过程分为四个阶段:初始标记,并发标记,重新标记,并发清理。其中初始标记和重新标记仍需要STW,并发标记和并发清理则可以与应用程序并发执行。
CMS垃圾回收器在与应用程序并发执行的过程中会争抢CPU资源,因此,默认情况下,CMS使用的并发线程数等于(CPU内核数+3)/4。
CMS垃圾回收器在与应用程序并发执行的过程中会争抢内存资源,因此,老年代在快要满但没有满的时候(这个内存比例,可以由JVM动态计算得到,也可以通过-XX:CMSInitiatingOccupancyFraction参数配置指定),虚拟机就要进行垃圾回收,这样做的目的是为并发执行的应用程序预留内存空间。如果预留空间不够,JVM将终止CMS的执行,使用Serial Old GC来进行本次的垃圾回收。
CMS采用的是标记清除算法,省去了整理空闲空间的时间,为了解决内存碎片化问题,CMS在经历过几次GC后,会进行一次内存碎片的整理。
G1 GC
G1全称叫做Garbage First。G1垃圾回收器是一个应用于整个堆上的垃圾回收器,它的实现方式跟其他垃圾回收器有较大差别。G1 GC最主要的设计目标是:将STW停顿的时间和分布,变成可预期且可配置的。
传统的分代垃圾回收算法避免了每次都要对整个堆进行垃圾回收,缩短了垃圾回收的时间,也缩短了STW的时间。
借鉴分代GC的思路,G1将整个堆划分为了多个区域(Region),一般是2048个。部分区域为新生代,部分区域为老年代。
G1把整个堆划分为多个小的区域,每个区域作为年轻代或者老年代,每次进行垃圾回收的时候,JVM只需要回收分代中一小部分区域,又进一步缩短了STW的时间。
G1 GC也是多线程进行垃圾回收,回收的过程可以与应用程序并发执行,年轻代采用标记-复制算法,老年代采用标记-清除算法。
跟其他垃圾回收器相比,G1垃圾回收器的STW时间是可预期的,我们可以通过JVM参数-XX:MaxGCPauseMillis设置可允许的最大STW时间。G1垃圾回收器会根据这个时间,来决定每次对多少个区域进行垃圾回收。理论上讲,STW时间设置的越小,每次进行垃圾回收的区域就越少,垃圾回收的频率就越高。
ZGC
ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:
- 停顿时间不超过10ms;
- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
- 支持8MB~4TB级别的堆(未来支持16TB)。
与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。
ZGC只有三个STW阶段:初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。
ZGC分为 6 个小阶段:
- 暂停—标记开始阶段:第一次暂停,标记根对象集合指向的对象;
- 并发标记/重映射阶段:遍历对象图结构,标记对象;
- 暂停—标记结束阶段:第二次暂停,同步点,弱根对象清理;
- 并发准备重定位阶段:引用处理、弱对象清理等;
- 暂停—重定位开始阶段:第三次暂停,根对象指向重定向集合;
- 并发重定位阶段:重定向集合中的对象重定向。
这 6 个阶段在绝大部分时间都是并发执行的,因此对应用运行的 GC 停顿影响很小。
Shenandoah GC
作为 ZGC 的另一个选择,Shenandoah 是一款超低延迟垃圾收集器(Ultra-Low-Pause-Time Garbage Collector),其设计目标是管理大型的多核服务器上,超大型的堆内存。GC 线程与应用线程并发执行、使得虚拟机的停顿时间非常短暂。
对应工作周期如下:
- 初始标记阶段(Init Mark):为堆和应用程序准备并发标记,然后扫描根对象集。这是 GC 周期的第一次暂停,持续时间取决于根对象集的大小。因为根对象集很小,所以速度很快,暂停非常短。
- 并发标记阶段(Concurrent Mark):并发标记遍历堆,并跟踪可到达的对象。该阶段与应用程序同时运行,其持续时间取决于存活对象的数量以及堆中对象图的结构。由于应用程序可以在此阶段自由分配新数据,因此在并发标记期间堆占用率会上升。
- 最终标记阶段(Final Mark):通过排空所有等待中的标记/更新队列,并重新扫描根对象集来完成并发标记。这是 GC 周期中的第二次暂停,这里最主要的时间消耗在排空队列并扫描根对象集合。
- 并发清理阶段(Concurrent Cleanup):并发清除会回收即时的垃圾区域,即在并发标记之后检测到的没有活动对象的区域。
- 并发转移阶段(Concurrent Evacuation):并发转移将对象从各个不同区域复制到指定区域。这是与其他 OpenJDK GC 的主要区别。此阶段与应用程序还是可以同时运行,持续时间取决于要复制的集合大小,不会导致程序暂停。
- 初始引用更新阶段(Init Update Refs):本阶段确保所有 GC 和应用程序线程均已完成转移,然后为下一阶段 GC 做准备。这是周期中的第三次暂停,是所有暂停中最短的一次。
- 并发引用更新阶段(Concurrent Update References):遍历堆,并发更新引用,并将引用更新为在并发转移期间移动的对象。这是与其他 OpenJDK GC 的主要区别。它的持续时间取决于堆中对象的数量,而不在乎对象图结构,因为它会线性扫描堆。此阶段与应用程序同时运行。
- 最终引用更新阶段(Final Update Refs):通过再次更新现有的根对象集合来完成更新引用阶段。这是 GC 周期中的最后一个暂停,其持续时间取决于根对象集的大小。
- 并发清理阶段(Concurrent cleanup):回收现阶段没有引用的区域。
Epsilon GC
Java 10版本中还新引入了Epsilon GC,可以通过参数-XX:+UseEpsilonGC开启。
Epsilon GC的目标是只分配内存,不执行垃圾回收,所以适合用来做性能分析,但无法用于生产环境。
因为不回收,所以在程序执行过程中也就没有GC消耗,性能测试更准确!