- 本文内容大部分根据书籍总结,本人没有具体考证1,个人所写的结论都会附带来源或证明,如有错误,欢迎指正,不胜感谢。
- 本文图片源自谷歌,如有侵犯,联系本人删除。
- 转载请于文首标明出处:【Java】JVM - 垃圾收集器 (juejin.cn)。
- 文章仍未完工,内容会逐步完善。
概览:
收集器 | 收集区域 | 收集方式 | 多线程 | STW | 特征 |
---|---|---|---|---|---|
Serial | 新生代 | 标记-复制 | 单线程 | 是 | 占用内存小 |
Serial Old | 老年代 | 标记-整理 | 单线程 | 是 | CMS 失败后备预案 |
Parallel Scavenge | 新生代 | 标记-复制 | 多线程 | 是 | 精确控制吞吐量 |
Parallel Old | 老年代 | 标记-整理 | 多线程 | 是 | 高吞吐量 |
ParNew | 新生代 | 标记-复制 | 多线程 | 是 | 唯一搭配 CMS,后植入 CMS |
CMS | 老年代 | 标记-清除 | 多线程 | 否 | 首个并发回收的收集器 |
其他:
- Grabage First
- ZGC
- Shenandoah
相关:
【Java】JVM - 垃圾回收算法 (juejin.cn)
Serial
Serial 是采用标记-复制算法的单线程新生代收集器。在 JDK1.3.1 之前是 HotSpot 新生代收集器的唯一选择。与其搭配使用的有 Serial Old 和 CMS 收集器。
由于它是单线程收集器,它的工作过程必将带来 STW。但对于内存资源受限的环境,它是所有垃圾收集器中额外内存占用(Memory Footprint)最小的;对于单核处理器或核心数较少的环境,由于它没有任何线程切换的开销反而会获得最高的单线程收集效率。因此Serial 适用于运行环境线程数少、分配给 JVM 管理的内存不多的场景,如用户桌面程序、部分微服务应用等客户端模式下的应用程序。
Serial Old
Serial Old 是采用标记-整理算法的单线程老年代收集器,其主要作用是搭配 Serial 在客户端模式下使用。如果在服务端模式下工作可能有两种用途:
- 在 JDK5 及之前的版本中与 PS Scavenge 搭配使用(不过会被称为 PS Scavenge)。
- 作为 CMS 发生失败的后备预案。
Serial + Serial Old 示意图:
Parallel Scavenge
Parallel Scavenge(PS Scavenge) 是采用标记-复制算法的多线程新生代收集器,其关注点在于尽可能提高用户程序的吞吐量。
对于垃圾收集程序而言,吞吐量 = 用户程序执行时间 / 总时间。总时间就是用户程序执行时间 + 垃圾收集程序执行时间。因此要提高吞吐量,总体方针就是提高用户程序执行时间的占比。
高吞吐量虽然不一定有良好的响应速度,但可以最高效率地利用 CPU 资源。因此 PS Scavenge 适合后台运算而不需要太多交互的任务。
与其配套的 Parallel Old 在 JDK6 才正式出现。此前常搭配 PS MarkSweep 进行老年代收集,由于 PS MarkSweep 与 Serial Old 共用一份代码,因此都流传为 PS Scavenge + Serial Old 组合。
参数:
-
-XX:MaxGCPauseMillis
:最大垃圾收集停顿时间,为大于 0 的毫秒数。 -
-XX:GCTimeRatio
:程序运行时间占总时间的比例,默认为 99。 -
-XX:+UseAdaptiveSizePolicy
:自调优开关,让 JVM 根据系统运行情况收集性能控制信息,自动设置垃圾回收参数。
Parallel Old
Parallel Old 是采用标记-整理算法的多线程老年代收集器,直到 JDK6 开始提供,在处理器资源稀缺或吞吐量优先的场合,都可以考虑使用 Parallel Scavenge + Parallel Old 组合进行垃圾收集。
ParNew
ParNew 是采用标记-复制算法的多线程新生代收集器。是 Serial 的多线程版本,它的工作方式和 Serial 基本完全一致,即使使用多线程进行 Minor GC,这个过程依旧需要 STW。它提供 -XX:ParallelGCThreads
参数来限制垃圾收集的线程数。
ParNew 是 JDK7 之前首选的新生代收集器,因为它是 Serial 废弃之后唯一能和 CMS 配合的收集器。而随着 CMS 被 G1 继承和取代,JDK9 取消了 ParNew 和 Serial Old 的组合,此后 ParNew 变成 CMS 的一部分,因为二者只能互相搭配使用,因此 ParNew 成为第一款退出历史舞台的 HotSpot 虚拟机。
CMS
CMS(Concurrent Mark Sweep)是采用标记-清除算法的多线程老年代收集器,支持并发收集。其收集过程分为四个步骤:
- 初始标记:是根节点枚举操作,必须 STW,但比较短暂。
- 并发标记:并发标记算法,使用增量更新的方式解决漏标问题,这样在并发标记时创建的对象也会被添加到重新标记的根节点中。
- 重新标记:为了保持一致性快照,必须 STW,但比较短暂。
- 并发清除:由于 CMS 采用标记-清除算法,非移动式回收,不会对用户线程造成影响,所以可以并发执行。
CMS 的问题
由于 CMS 支持并发收集,导致 GC 的停顿时间非常短暂,因此 CMS 适合工作在关注服务响应速度的程序中,如 Web 服务器上。但 CMS 仍然存在三个较大的问题:
-
CPU 敏感:面向并发设计的垃圾收集器都是 CPU 敏感的,由于分出一部分线程进行垃圾收集,会导致用户程序变慢降低吞吐量。CMS 默认启用
(CPU cores + 3) / 4
个线程进行回收工作,CPU 核心数越低,造成的吞吐量下降就越大。为缓解该情况,JVM 提供了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS) 的 CMS 变种,通过让 GC 线程与用户线程交替运行的方式,增加停顿时间来换取吞吐量。但由于效果不明显在 JDK8 中被声明废弃,JDK9 中被废弃。
-
浮动垃圾问题:由于并发清除阶段用户程序仍在运行,当垃圾回收的速度赶不上内存分配的速度,就出现“并发失败(Concurrent Mode Failure)”,此时就会触发一次 Full GC,并调用 Serial Old 整理内存。并发失败的情况有:
-
应用程序创建大对象,向老年代申请空间,此时空间不足。
-
新生代进行 Minor GC,向老年代申请分配担保确认,此时空间不足以进行分配担保。
-
方法区空间耗尽,通常 CMS 不会回收方法区,但是空间不足时会促发 Full GC 回收方法区。
-XX:CMSInitiatingOccu-pancyFraction
参数设置老年代回收的阈值,JDK5 中默认为 68%,JDK6 中默认为 92%。
CMSInitiatingPermOccupancyFraction
可以启用 CMS 对方法区进行收集。对方法区的收集和对老年代的收集是相互独立的。 -
-
内存碎片问题。由于采用标记-清除算法,收集结束后会出现大量的内存碎片。当无法分配足够大的连续空间时,JVM 不得不触发一次 Full GC。例如如果 Minor GC 向老年代申请分配担保时发现内存足够,但是由于内存碎片的原因无法容纳对象,就会发生“晋升失败(Promotion Failed)”的错误。
-XX:UseCMSCompactAtFullCollection
开关参数用于在 CMS 必须进行 Full GC 时开启内存整理功能。
-XX:CMSFullGCsBeforeCompaction
参数用于要求 CMS 在执行若干次没有内存整理的 Full GC 之后,下一次进入 Full GC 之前进行碎片整理,默认为 0 表示每一次都整理。这两个参数都在 JDK9 中被废弃。
废弃
CMS 在 JDK8 中被标注为不推荐使用,在 JDK9 中被宣布废弃,取而代之的是 G1 垃圾收集器。虽然被宣布废弃,但直到 JDK14 才被真正的移除,并不是人云亦云的 JDK9 后就不能用了。
-
JDK9:
$ java -XX:+UseConcMarkSweepGC -version Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release. java version "9.0.4" Java(TM) SE Runtime Environment (build 9.0.4+11) Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)
-
JDK13:依旧可以使用,但同样伴随着警告。
& 'C:\Program Files\Java\jdk-13.0.2\bin\java.exe' -XX:+UseConcMarkSweepGC -version Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release. java version "13.0.2" 2020-01-14 Java(TM) SE Runtime Environment (build 13.0.2+8) Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)
-
JDK14:正式被移除
& 'C:\Program Files\Java\jdk-14.0.2\bin\java.exe' -XX:+UseConcMarkSweepGC -version Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; support was removed in 14.0 java version "14.0.2" 2020-07-14 Java(TM) SE Runtime Environment (build 14.0.2+12-46) Java HotSpot(TM) 64-Bit Server VM (build 14.0.2+12-46, mixed mode, sharing)
Garbage First
G1(Garbage First)是首个面向局部收集和基于 Region 内存布局的垃圾收集器。被官方称为“全功能的垃圾收集器”。是一款主要面向服务端应用的垃圾收集器。
基于 Region 的布局
G1 把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个区域根据需要扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。这使得收集器能够对扮演不同角色的区域采用不同的策略去处理。
同时,G1 专门设置了 Humongous 区域用于存储大对象。只要大小超过 Region 的一半就会被判定为大对象。每个 Region 大小根据 -XX:C1HeapRegionSize
设置。对于大小超过 Region 的对象,会使用连续 N 个 Humongous 区域进行存放。Humongous 区域在 G1 中一般被看作是老年代区域。
真是这种基于堆的布局使得 G1 可以面向堆内存的任何部分来组成回收集(Collection Set,CSet),衡量标准不再是那个分代,而是那块内存更值得回收,这就是 G1 收集器的 Mixed GC 模式。
分区下的记忆集
G1 的每个 Region 都会维护一个记忆集,记录别的 Region 指向自己的指针。这个记忆集实际上就是一个哈希表,键是其他 Region 的地址偏移,值是当前区域卡表页索引号的数组。Region 越多,跨区指针就会越多,同时由于 G1 存储所有的跨区指针,所以 G1 卡表的内存占用负担是非常中的。通常 G1 要消耗 Java 堆 10% ~ 20% 的额外内存来维持收集器工作。
可预测的停顿时间模型
G1 将 Region 视为单词回收的最小单元,同时 G1 会跟踪各个 Region 里面的垃圾收集的统计信息,然后建立一个以衰减平均值(Decaying Average)为基础的停顿时间预测模型。根据这个停顿模型,G1 可以预测每一块 Region 垃圾收集的收益(可回收的空间与回收时间的比值),然后按照收益的大小维护一个优先级列表。每次回收时根据用户设定允许的收集停顿时间,在停顿时间允许的范围内,挑选出回收价值最大的那些 Region 进行回收,这也是“Garbage First”的由来。
衰减平均值会受到新数据的影响,平均值代表整体平均状态,衰减平均值代表最近的平均状态,因此更能反映目前的回收效果。
并发标记
G1 通过原始快照(SATB)算法解决并发标记的漏标问题。对于新增加对象的处理,G1 在每个 Region 上设计了两个名为 TAMS(Top at Mark Start) 的指针,将 Region 的一部分空间划分出来用于新对象的分配。G1 收集时默认这个地址上的对象都是被隐式标记过的,不属于垃圾对象。
运行过程
G1 的运行过程主要分为四个步骤:
- 初始标记:进行根节点枚举和设置 TAMS 指针,基本没有额外停顿。
- 并发标记:进行并发可达性分析,耗时较长,但是与程序并发执行,使用 SATB 处理漏标问题。
- 最终标记:保持一直性快照,必须 STW。
- 筛选回收:对各个 Region 的回收价值进行统计排序,根据用户期望停顿时间制定回收计划。回收 Region 时会把存活对象复制到空的 Region 中,再清理掉原来整个 Region 的空间。由于复制操作涉及到对象的移动,必须暂停用户线程。
筛选回收之所以不设计为并发执行,是因为 G1 的整体理念是在用户允许的停顿时间内进行垃圾回收,回收时间是可控的,因此,不必过分苛求低延迟垃圾回收。同时,牺牲小部分 STW 时间可以提高垃圾回收的吞吐量,这使得 G1 成为“全能垃圾收集器”。
G1 与 CMS 对比
二者的共同点:
- 都是支持并发收集的垃圾收集器。
- 都非常成功,使用非常普遍,一个是昔日的王者,一个是新晋的王者。
G1 的优势:
- 可以指定最大停顿时间。
- 内存按区域划分,按收益动态回收垃圾更加高效。
- 使用标记-复制算法,不会存在内存碎片问题。
- 使用原始快照并发标记速度更快。
- 不会那么容易触发 Full GC。
CMS 的优势:
- 更低的内存消耗,记忆集不需要存储新生代对老年代的引用,更加符合对象朝生夕灭的规律。
- 更低的执行复杂,少了非常多额外的运算和处理。
- 回收阶段可以并发执行,垃圾收集的延迟非常低。
孰优孰劣是不用说的,毕竟 CMS 已经被淘汰了,但这只是因为人们的选择,并非是说 CMS 有多差。
Footnotes
-
周志明.深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)[M].北京:机械工业出版社,2019.89-121 ↩