# 浅析Hotspot的经典7种垃圾收集器原理特点与组合搭配
HotSpot共有7种垃圾收集器,3个新生代垃圾收集器,3个老年代垃圾收集器,以及G1,一共构成7种可供选择的垃圾收集器组合。
新生代与老年代垃圾收集器之间形成6种组合,每个新生代垃圾收集器都对应2种组合。
新生代垃圾收集器
所有新生代垃圾收集器,都使用复制算法,都会发生stop-the-world。由于绝大多数对象的生命周期通常比较短,在新生代被回收的可能性很大,新生代的垃圾回收通常可以回收大部分对象,因此采用复制算法效率更高。
Serial
采用复制算法,GC时发生stop-the-world,使用单个GC线程。
“Serial” is a stop-the-world, copying collector which uses a single GC thread.
特点:
- 客户端模式下的默认新生代收集器
- 单线程工作(它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束)
- 简单而高效(与其他收集器的单线程相比)
- 对于内存资源受限的环境, 它是所有收集器里额外内存消耗(Memory Footprint)最小的
- 对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
⠀Serial/Serial Old收集器协同工作运行示意图如下:
ParNew
采用复制算法,GC时发生stop-the-world,使用多个GC线程。
ParNew 与 Parallel Scavenge的一个主要区别是,ParNew可以与CMS进行搭配使用。
“ParNew” is a stop-the-world, copying collector which uses multiple GC threads. It differs from “Parallel Scavenge” in that it has enhancements that make it usable with CMS. For example, “ParNew” does the synchronization needed so that it can run during the concurrent phases of CMS.
特点:
- Serial收集器的多线程并行版本(除了同时使用多条线程进行垃圾收集之外, 其余的行为(包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等)都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码)
- JDK 7之前在服务端模式下首选的新生代收集器(一个重要原因:除了Serial收集器外, 目前只有它能与 CMS 收集器配合工作)
- ParNew收集器是激活CMS后(使用-XX: +UseConcMarkSweepGC选项) 的默认新生代收集器,也可以使用-XX: +/-UseParNewGC选项来强制指定或者禁用它。
- ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果
- 默认开启的收集线程数与处理器核心数量相同(可以使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数)
Parallel Scavenge
采用复制算法,GC时发生stop-the-world,使用多个GC线程。 吞吐量优先收集器,可控制最大垃圾收集停顿时间(-XX:MaxGCPauseMillis)与吞吐量大小(-XX:GCTimeRatio),支持GC自适应的调节策略(GC Ergonomics,对应参数-XX:+UseAdaptiveSizePolicy)。
“Parallel Scavenge” is a stop-the-world, copying collector which uses multiple GC threads.
特点:
- 新生代收集器
- 基于标记-复制算法实现
- 多线程收集器
- 吞吐量优先收集器(Parallel Scavenge收集器其他收集器不同在于:CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量)
- 自适应调节策略(通过+UseAdaptiveSizePolicy参数激活)
ParNew和Parallel Scavenge基本没什么区别,都是年轻代的垃圾回收器,就是在Parallel Scavenge的基础上做了一些简单的增强使其能够较好的配合CMS的使用,ParNew是Parallel Scavenge的一个变种
ParNew响应时间优先,Parallel Scavenge吞吐量优先
吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
通常来说,停顿时间越短(低延迟)就越适合需要与用户交互或需要保证服务响应质量的程序, 良好的响应速度能提升用户体验;
而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务, 主要适合在后台运算而不需要太多交互的分析任务。
老年代垃圾收集器
除了CMS,其他的老年代垃圾收集器GC时都是stop-the-world,都会在清理垃圾之后进行压缩整理。
Serial Old
采用标记整理算法,GC时发生stop-the-world,使用单个GC线程。
“Serial Old” is a stop-the-world, mark-sweep-compact collector that uses a single GC thread.
特点:
- Serial收集器的老年代版本
- 单线程收集器
- 使用标记-整理算法
- 主要在客户端模式下使用。如果在服务端模式下,它也可能有两种用途: 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
⠀Serial/Serial Old收集器协同工作运行示意图如下:
Parallel Old
采用标记整理算法,GC时发生stop-the-world,使用多个GC线程。
“Parallel Old” is a compacting collector that uses multiple GC threads.
特点:
- Parallel Scavenge收集器的老年代版本
- 多线程并发收集
- 使用标记-整理算法
- 在注重吞吐量或者处理器资源较为稀缺的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
整合和压缩算法
对内存进行整合和压缩是指在垃圾回收过程中对内存中的对象进行整理和重新排列,通过将存活对象向一端移动,以创建更大的连续内存空间,减少内存碎片并提高内存利用率的过程。这通常涉及将对象复制到新的内存空间,然后将原来的内存空间释放。这个过程通常发生在老年代的垃圾回收中。
⠀Parallel Scavenge/Parallel Old 收集器运行示意图如下:
CMS
CMS采用标记清理算法,是一个以低暂停时间为目标的垃圾收集器。GC时大部分时间并发执行,其中初始化标记和重新标记两个阶段仍然会发生stop-the-world,其余阶段都是并发执行。
“CMS” is a mostly concurrent, low-pause collector.
Java之CMS GC的7个阶段:https://mp.weixin.qq.com/s/vmnBlrM7pTtVuyQU-GTcPw
收集流程:
CMS整个过程的四个步骤如下,其中初始标记、 重新标记这两个步骤仍然需要“Stop The World”。
1. 初始标记-仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
2. 并发标记-从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
3. 重新标记-为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短
4. 并发清除-清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的
Concurrent Mark Sweep 收集器运行示意图如下:
缺点:
- CMS 收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。
- 由于CMS收集器无法处理“浮动垃圾”, 有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
- CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。
关于CMS收集器浮动垃圾的说明:
由于在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
由于在垃圾收集阶段用户线程还需要持续运行, 那就还需要预留足够内存空间提供给用户线程使用, 因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
G1
G1将整个堆划分为多个大小相等的独立区域(Region),保留新生代和老年代的分代概念(但两者不再是物理隔离的)。
从整体来看是基于标记整理算法,从局部(两个Region之间)来看是基于复制算法。因此,可以避免产生内存空间碎片,防止发生并发模式失败。
使用多个GC线程,每次优先回收价值最大的Region。
支持可预测的停顿时间模型,从而提高收集效率,降低stop-the-world的时间。
The Garbage First or G1 garbage collector is available in Java 7 and is designed to be the long term replacement for the CMS collector. The G1 collector is a parallel, concurrent, and incrementally compacting low-pause garbage collector that has quite a different layout from the other garbage collectors described previously.
特点:
- 将堆内存“化整为零”,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
- 面向全堆的收集器,不再需要其他新生代收集器的配合工作
- 主要面向服务端应用的垃圾收集器
- G1 仍是遵循分代收集理论设计的, 但其堆内存的布局与其他收集器有非常明显的差异
- G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立Region,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间
- 收集器能够对扮演不同角色的Region采用不同的策略去处理, 这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果
- Region中还有一类特殊的Humongous区域, 专门用来存储大对象。 G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。 每个Region的大小可以通过参数-XX: G1HeapRegionSize设定, 取值范围为1MB~32MB, 且应为2的N次幂。 而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中, G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待
- G1收集器之所以能建立可预测的停顿时间模型, 是因为它将Region作为单次回收的最小单元, 即每次收集到的内存空间都是Region大小的整数倍, 这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集
- G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小, 价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表, 每次根据用户设定允许的收集停顿时间(使用参数-XX: MaxGCPauseMillis指定, 默认值是200毫秒),优先处理回收价值收益最大的那些Region
- 可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡(默认的停顿目标为两百毫秒,通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的)
G1 收集器 Region 分区示意图如下:
G1 收集器的运作过程主要步骤如下:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
⠀从上述阶段的描述可以看出, G1收集器除了并发标记外, 其余阶段也是要完全暂停用户线程的,换言之, 它并非纯粹地追求低延迟, 官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
G1 收集器运行示意图如下:
G1 收集器与 CMS 收集器的异同
- 两者都非常关注停顿时间的控制
- G1 可以指定最大停顿时间、 分Region的内存布局、 按收益动态确定回收集
- 与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集
- G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间,而CMS的卡表就相当简单,只有唯一一份, 只需要处理老年代到新生代的引用, 反过来则不需要
- 它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。
垃圾回收器器组合搭配
Serial Old(MSC)可以与所有新生代收集器进行组合,共3种组合
JVM仅指定新生代垃圾收集器的情况下,默认老年代采用Serial Old垃圾收集器(带压缩):
-XX:+UseSerialGC
Serial (DefNew) + Serial Old(Serial Mark Sweep Compact)
-XX:+UseParNewGC
Parallel (ParNew) + Serial Old(Serial Mark Sweep Compact)
-XX:+UseParallelGC
Parallel Scavenge (PSYoungGen) + Serial Old(Serial Mark Sweep Compact (PSOldGen))
注:在Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,但由于PS MarkSweep与Serial Old实现非常接近,因此官方的许多资料都直接以Serial Old代替PS MarkSweep进行讲解。
Parallel Old(带压缩)只能与Parallel Scavenge进行组合
-XX:+UseParallelOldGC
Parallel Scavenge (PSYoungGen) + Parallel Mark Sweep Compact (ParOldGen)
CMS(不带压缩)可以与Serial和ParNew进行组合,共2种组合
-XX:-UseParNewGC -XX:+UseConcMarkSweepGC
Serial (DefNew) + CMS(Concurrent Mark Sweep)
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Parallel (ParNew) + CMS(Concurrent Mark Sweep) + Serial Old(Serial Mark Sweep Compact)
G1(Garbage First),不需要搭配其他垃圾收集器
-XX:+UseG1GC
6种垃圾收集组合关系图
推荐使用的2种GC组合
1.基于低停顿时间的垃圾收集器
-XX:+UseConcMarkSweepGC(该参数隐式启用-XX:+UseParNewGC)
2.基于吞吐量优先的垃圾收集器
-XX:+UseParallelOldGC(该参数隐式启用-XX:+UseParallelGC)
总结
下面对这七种垃圾收集器进行了一个总结,具体如下表所示。
收集器 | 串行、并行、并发 | 新生代/老年代 | 算法 | 目标 | 使用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时再Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式 CMS的后备预案 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上 |
G1 | 并发 | 新生代/老年代 | 标记-整理/复制算法 | 响应速度优先 | 面向服务端应用,用来替换CMS |
参考
HotSpot的7种垃圾收集器组合
浅析经典JVM垃圾收集器-Serial/ParNew/Parallel Scavenge/Serial Old/Parallel Old/CMS/G1
https://blogs.oracle.com/
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://www.splunk.com/en_us/products/apm-application-performance-monitoring.html?source=plumbr
《深入理解Java虚拟机 JVM高级特性与最佳实践》周志明
《Java性能权威指南》
Java之CMS GC的7个阶段:https://mp.weixin.qq.com/s/vmnBlrM7pTtVuyQU-GTcPw