垃圾收集器
HotSpot 包含的垃圾收集器
一、串行收集器
1.1、Serial 收集器
- 在 JDk1.3 之前是新生代收集的唯一选择
- 单线程,只会使用一个 CPU 去完成
- 垃圾收集时,必须暂停其他工作线程,直到它收集结束。“Stop The World”
- 虚拟机后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉
到现在为止,是虚拟机运行在 Client 模式下的默认新生代收集器 - 通过 JVM 参数-XX:+UseSerialGC 可以使用串行垃圾回收器。
1.2、Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
主要意义也是在于给 Client 模式下的虚拟机使用
-
如果在 Server 模式下,那么它主要还有两大用途:
- 在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
Serial/Serial Old 收集器运行示意图
二、并行收集器
2.1、并行与并发的概念
并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。
2.2、ParNew 收集器
ParNew 收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致。
由于存在线程交互的开销,ParNew 收集器在单 CPU 环境下性能并没有单线程垃圾收集器性能好。
-
虚拟机启动参数
-
-XX:+UseParNewGC
: 新生代使用 ParNew 回收器,老年代使用串行回收器 -
-XX:+UseConcMarkSweepGC
新生代使用 ParNew 回收器,老年代使用 CMS -
-XX:ParallelGCThreads
参数来限制垃圾收集的线程数
-
- ParNew/Serial Old 收集器运行示意图
2.3、Parallel Scavenge 收集器
Parallel Scavenge 收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)
-
吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值
-
虚拟机参数
-
-XX:+UseParallelGC
新生代使用 ParallelGC 回收器,老年代使用串行回收器 -
-XX:+UseParallelOldGC
新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器 -
-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间(值大于0)。设置值越小停顿的时间越小,但会导致 GC 频繁,且增加 GC 总时间,从而降低吞吐量。 -
-XX GCTimeRatio
设置吞吐量大小,参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数 -
-XX:+UseAdaptiveSizePolicy
可以打开自适应 GC 策略。就不需要人工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数。
-
2.4、Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到 JDK 6 时才开始提供的。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 ParallelScavenge 加 Parallel Old 收集器这个组合
- Parallel Old 收集器的工作过程如图
三、CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。也是基于标记—清除算法实现的。
3.1、运作过程
- 1)初始标记(CMS initial mark)—— 仍然需要“Stop The World”,标记一下 GC Roots 能直接关联到的对象,速度很快。
- 2)并发标记(CMS concurrent mark)—— 并发标记阶段就是从 GCRoots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
- 3)重新标记(CMS remark)—— 仍然需要“Stop The World”,重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
- 4)并发清除(CMS concurrent sweep)——清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
3.2、运行示意图
3.3、优缺点
- 优点:并发收集、低停顿
- 缺点
- CMS 收集器对处理器资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。
- 由于 CMS 收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生。
- CMS 是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
浮动垃圾:在 CMS 的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS 无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
3.4 虚拟机参数
-XX:+UseConcMarkSweepGC
启用 CMS-XX:-CMSPrecleaningEnabled
关闭预清理-XX:ConcGCThreads
、-XX:ParallelCMSThreads
设置并发线程数量,CMS 的默认并发线程数是(ParallelGCThreads+3 )/4。 ParallelGCThreads 表示 GC 并行时使用的线程数 ,如果新生代使用 ParNew ,那么 ParallelGCThreads 也就是新生代 GC 的线程数量。-
XX:CMSInitiatingOccupancyFraction
: CMS 的触发百分比,- 在 JDK 5 中默认当老年代使用了 68%的空间后就会被激活,JDK 6 时提高到了 92%。
- 提高 CMS 的触发百分比,可以降低内存回收频率,获取更好的性能。
- 触发百分比过高会面临另一种风险:要是 CMS 运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
-XX:+UseCMS-CompactAtFullCollection
:(默认是开启的,此参数从 JDK 9 开始废弃),用于在 CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在 Shenandoah 和 ZGC 出现前)是无法并发的。-XX:CMSFullGCsBefore-Compaction
:(此参数从 JDK 9 开始废弃),这个参数的作用是要求 CMS 收集器在执行过若干次(数量由参数值决定)不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为 0,表示每次进入 Full GC 时都进行碎片整理)。
四、Garbage First 收集器(G1)
4.1 G1 介绍
Garbage First(简称 G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。G1 是一款主要面向服务端应用的垃圾收集器。
4.2、G1 设计思路
- G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region)
- 每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间
- 收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
- Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待。
4.3、G1 内存布局形式示意图
4.4 G1 收集器的运作过程
- 初始标记: 标记从根节点直接可达的对象 这个阶段会伴随一次新生代 GC ,它是会产生全局停顿的,应用程序线程在这个阶段必须停止执行
- 根区域扫描:这个过程是可以和应用程序并发执行的是根区域扫描不能和新生代 GC 同时执行 (因为根区域扫描依赖 survivor 区的对象,而新生代 GC 会修改这个区域),因此如果恰巧在此时需要进行新生代 GC, GC 就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代 GC 的时间就会延长
- 井发标记::和 CMS 类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发的过程,并且这个过程可以被一次新生代 GC 打断
- 重新标记:和 CMS 一样,重新标记也是会产生应用程序停顿的。由于在并发标记过程中,应用程序依然在运行,因此标记结果可能需要进行修正,所以在此对上一次的标记结果进行补充。在 G1 中,这个过程使用 SATB (Snapshot At-The-Beginning ,原始快照)算法完成,G1 会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
- 独占清理:这个阶段是会引起停顿的, 它将计算各个区域的存活对象和 GC 回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set)该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。
- 井发清理阶段:这里会识别并清理完全空闲的区域,它是井发的清理,不会引起停顿
4.5、虚拟机参数
-XX:G1HeapRegionSize
用于设置 Region 区域的大小-XX:MaxGCPauseMillis
设定允许的收集停顿时间,默认值是 200 毫秒-XX:+UseG1GC
启用 G1 回收器
六、低延迟收集器
从上图中可以看到,最后的两款收集器,Shenandoah 和 ZGC,几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系。
6.1 Shenandoah 收集器
-
与 G1 的不同
- 支持并发的整理算法,G1 的回收阶段是可以多线程并行的,但却不能与用户线程并发
- Shenandoah(目前)是默认不使用分代收集的
- 摒弃了在 G1 中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(ConnectionMatrix)的全局数据结构来记录跨 Region 的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。
Shenandoah 收集器的连接矩阵示意图
-
Shenandoah 收集器的工作过程(并发标记、并发回收、并发引用更新)
- 初始标记(Initial Marking):与 G1 一样,首先标记与 GC Roots 直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与 GCRoots 的数量相关。
- 并发标记(Concurrent Marking):与 G1 一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
- 最终标记(Final Marking):与 G1 一样,处理剩余的 SATB 扫描,并在这个阶段统计出回收价值最高的 Region,将这些 Region 构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。
- 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的 Region(这类 Region 被称为 Immediate GarbageRegion)。
- 并发回收(Concurrent Evacuation):并发回收阶段是 Shenandoah 与之前 HotSpot 中其他收集器的核心差异。在这个阶段,Shenandoah 要把回收集里面的存活对象先复制一份到其他未被使用的 Region 之中
- 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。
- 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。
- 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于 GC Roots 中的引用。这个阶段是 Shenandoah 的最后一次停顿,停顿时间只与 GC Roots 的数量相关。
- 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的 Region 已再无存活对象,这些 Region 都变成 Immediate GarbageRegions 了,最后再调用一次并发清理过程来回收这些 Region 的内存空间,供以后新对象分配使用。
-
工作过程图示
6.2 ZGC 收集器
ZGC 和 Shenandoah 的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
-
ZGC 的 Region
- 小型 Region(Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。
- 中型 Region(Medium Region):容量固定为 32MB,用于放置大于等于 256KB 但小于 4MB 的对象。
- 大型 Region(Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。
-
ZGC 的堆内存布局
-
染色指针技术(Colored Pointer)
染色指针技术是一种直接将少量额外的信息存储在指针上的技术。目前在 Linux 下 64 位的操作系统中高 18 位是不能用来寻址的,但是剩余的 46 为却可以支持 64T 的空间,到目前为止我们几乎还用不到这么多内存。于是 ZGC 将 46 位中的高 4 位取出,用来存储 4 个标志位,剩余的 42 位可以支持 4TB(2 的 42 次幂)的内存,也直接导致 ZGC 可以管理的内存不超过 4TB,如图所示:
-
染色指针的三大优势
- 染色指针可以使得一旦某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用掉,而不必等待整个堆中所有指向该 Region 的引用都被修正后才能清理
- 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量
- 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能
-
染色指针图示
-
ZGC 运作过程
- 并发标记(Concurrent Mark):与 G1、Shenandoah 一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于 G1、Shenandoah 的初始标记、最终标记(尽管 ZGC 中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与 G1、Shenandoah 不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的 Marked 0、Marked 1 标志位。
- 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set)
- 并发重分配(Concurrent Relocate):重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
- 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用
ZGC 运作图示