HotSpot垃圾收集器完整原理与JDK7/8选型
垃圾收集器不是“自动删除没用对象”的一个后台线程,而是一套同时解决以下问题的运行时系统:
- 怎样从GC Roots判断对象是否仍然存活。
- 怎样在业务线程不断修改引用时得到可信的对象图。
- 怎样回收空间、整理碎片并更新对象引用。
- 怎样在吞吐量、暂停时间、内存占用和CPU开销之间取舍。
- 常规回收赶不上分配速度时,怎样退化、失败或最终抛出OOM。
本页以企业中仍然非常重要的JDK 7、JDK 8为主线,再说明JDK 9+的变化。学习前建议先看垃圾收集策略与算法和对象分配、TLAB、晋升与GC。
学习目标
学完后应能解释:
- Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1分别解决什么问题。
- “串行、并行、并发、Stop The World”为什么不是同一组概念。
- JDK 7/8常见收集器组合、默认值和JDK 9+版本边界。
- CMS为什么需要初始标记、并发标记、重新标记和并发清除。
- 并发标记时业务线程改了引用,CMS和G1怎样避免漏标。
- G1为什么仍然有逻辑上的Eden、Survivor和Old,Region解决了什么问题。
- Card Table、Remembered Set、写屏障、SATB分别解决什么问题。
- Young GC、CMS周期、G1 Mixed GC和Full GC怎样发生。
- Promotion Failed、Concurrent Mode Failure、To-space exhausted和Humongous分配问题怎样区分。
- 如何用JDK 7/8日志参数运行同一个Demo并比较收集器。
- 商业服务应该如何基于SLO、堆大小、CPU和分配特征选型,而不是背“某GC最好”。
一、先分清四个词
1.1 Stop The World
STW表示Java业务线程在某个安全点或安全区域暂停,让JVM完成不能与业务代码同时安全执行的工作。它不表示操作系统所有进程停止,也不表示整个GC周期都一定暂停。
例如CMS的并发标记阶段可与业务线程并发,但初始标记和重新标记仍然需要STW。G1的并发标记也不是“零停顿”。
1.2 串行与并行
- 串行收集:STW阶段主要由单个GC工作线程执行,例如Serial。
- 并行收集:STW阶段由多个GC工作线程协同执行,例如Parallel Scavenge。
并行不等于并发。Parallel GC虽然有多个回收线程,但回收时业务线程通常仍被暂停。
1.3 并发
并发表示某些GC阶段和业务线程在时间上同时推进,例如CMS Concurrent Mark、G1 Concurrent Mark。它能缩短最长停顿,但会带来:
- GC线程与业务线程竞争CPU和内存带宽。
- 业务线程继续分配,产生浮动垃圾。
- 引用仍在变化,需要写屏障和并发标记算法维持正确性。
- 回收速度若赶不上分配速度,可能发生退化或Full GC。
flowchart TD
A["GC周期开始"] --> B["短暂STW建立可靠起点"]
B --> C["并发阶段:GC与业务共同运行"]
C --> D["短暂STW修正并发期间变化"]
D --> E["回收或转移对象"]
E --> F["业务继续运行"]二、为什么不存在适合所有系统的收集器
收集器要同时面对四个互相牵制的目标:
| 目标 | 含义 | 典型业务 |
|---|---|---|
| 吞吐量 | 用户代码时间占总时间比例尽量高 | 离线计算、报表、批处理 |
| 暂停时间 | 单次STW尽量短且P99可控 | 网关、支付、在线接口 |
| 内存占用 | GC辅助结构、预留空间尽量少 | 小容器、边缘节点 |
| CPU成本 | GC不长期抢占业务CPU | CPU紧张或高密度部署 |
降低暂停常常需要更频繁回收、更多并发线程、更多Region/记忆集元数据和更大的空闲余量;提高吞吐常常允许更长的集中暂停。选型必须先明确业务SLO,不能只问“哪个GC最快”。
三、JDK7、JDK8与JDK9+版本地图
| 收集器 | JDK 7/8地位 | 后续变化 | 典型定位 |
|---|---|---|---|
| Serial / Serial Old | 可用 | 仍可用 | 小堆、单核、工具进程 |
| ParNew | 常与CMS搭配 | JDK 9起组合受限,后续逐渐退出主线 | CMS年轻代伙伴 |
| Parallel Scavenge / Parallel Old | JDK 7/8服务端常见默认组合,但必须以实际Ergonomics为准 | JDK 9默认不再是它 | 吞吐优先 |
| CMS | JDK 7/8低停顿常用方案 | JDK 9废弃,JDK 14移除 | 历史低停顿方案 |
| G1 | JDK 7u4引入,JDK 8逐步成熟;不是JDK 8所有发行版默认 | JDK 9起HotSpot常见默认 | 大堆、可预测停顿目标 |
| ZGC | JDK 7/8不存在 | JDK 11引入,后续持续演进 | 超大堆、极低停顿 |
| Shenandoah | 主流Oracle JDK 7/8没有 | 不同OpenJDK发行版和版本支持不同 | 并发转移、低停顿 |
“JDK 8默认一定是Parallel”也不能脱离上下文绝对化。默认由Server/Client VM、CPU、内存、操作系统、发行版和启动参数共同决定。生产应直接查看:
java -version
java -XX:+PrintCommandLineFlags -version运行中进程可使用:
jcmd <pid> VM.flags
jcmd <pid> VM.command_lineJDK 7早期更新版的工具能力和参数支持可能不同,必须记录完整版本,例如1.7.0_80、1.8.0_202,不能只记录“JDK8”。
四、收集器组合为什么重要
JDK 7/8经典分代收集器通常要同时选择年轻代和老年代方案:
flowchart TD
A["Serial年轻代"] --> B["Serial Old老年代"]
C["ParNew年轻代"] --> D["CMS老年代"]
E["Parallel Scavenge年轻代"] --> F["Parallel Old老年代"]
G["G1统一管理Region"] --> H["Region动态承担Eden/Survivor/Old"]并不是任意年轻代和老年代收集器都能自由组合。它们在对象晋升、数据结构、屏障和执行框架上需要兼容。G1虽然统一管理整堆,但仍保留逻辑分代,并不是“没有新生代和老年代”。
五、Serial与Serial Old
5.1 Serial Young过程
Serial年轻代收集器通常使用复制思想:
flowchart TD
A["Eden分配失败"] --> B["业务线程到达安全点并STW"]
B --> C["单个GC线程从Roots和老年代跨代引用出发"]
C --> D["复制存活对象到To Survivor或老年代"]
D --> E["更新引用并清空Eden与From"]
E --> F["From/To交换并恢复业务线程"]优点:实现简单,没有多GC线程协调开销,小堆或CPU很少时可能非常有效。缺点:堆和存活对象增大后,单线程STW容易变长。
启用:
-XX:+UseSerialGC它通常同时选择Serial Young和Serial Old。不要把“Serial适合客户端”理解成只能在桌面程序使用;小型命令行工具、测试任务和极小容器也可能适合,最终以停顿和吞吐实测为准。
5.2 Serial Old过程
Serial Old面向老年代,典型思想是标记—整理:
- STW后从GC Roots做可达性分析。
- 标记存活对象。
- 移动并压紧存活对象,消除外部碎片。
- 更新所有受影响引用。
- 恢复业务线程。
它还可能成为CMS并发失败时的退化后备路径,所以使用CMS并不代表永远不会见到Serial Old式长暂停。
六、ParNew
ParNew可以理解为Serial年轻代收集逻辑的并行版本:业务线程STW后,多条GC工作线程并行扫描和复制存活对象。
-XX:+UseParNewGCJDK 7/8中它最重要的历史价值是与CMS搭配。它并不是“多线程就一定比Serial快”:
- 小堆、单核或CPU受限时,线程协调成本可能得不偿失。
- GC线程数过高会争抢CPU。
- 存活对象很多时,复制和晋升成本仍然高。
- 老年代压力不会因为年轻代并行就消失。
JDK 8可用-XX:ParallelGCThreads影响STW并行GC线程数量,但生产不应脱离CPU配额盲目设置。
七、Parallel Scavenge与Parallel Old
7.1 目标是总体吞吐,不是每次暂停最短
吞吐量定义为:
吞吐量 = 业务线程运行时间 / (业务线程运行时间 + GC时间)Parallel Scavenge在年轻代并行复制,Parallel Old在老年代并行标记整理。启用组合:
-XX:+UseParallelGCJDK 8中通常会配合Parallel Old;具体选择仍应查看启动日志和Flags。
7.2 三个容易写错或误解的参数
-XX:MaxGCPauseMillis=200
-XX:GCTimeRatio=99
-XX:+UseAdaptiveSizePolicy注意参数叫GCTimeRatio,不是GCTimeRadio。
MaxGCPauseMillis是软目标,不是SLA保证。JVM可能调整代大小来尝试接近它。GCTimeRatio=99常表达GC时间占比目标约为1 / (1 + 99),也就是约1%。它不是“GC占99%”。- Adaptive Size Policy会根据运行反馈调整年轻代、Eden/Survivor比例和晋升相关策略;手工固定过多参数可能限制自适应空间。
同时要求极低暂停和极高吞吐可能互相冲突。JVM只能在实际对象存活率、CPU和堆容量约束下尽量优化。
7.3 商业适用场景
日终批量对账、离线ETL、索引重建等任务通常更关心总完成时间。如果没有外部请求等待,偶尔较长暂停可能可接受,Parallel系列经常比并发收集器更符合目标。
八、CMS完整执行过程
CMS全称Concurrent Mark Sweep,目标是让老年代的大部分标记和清除工作与业务线程并发,降低长时间STW概率。它主要使用标记—清除,不在普通并发清除阶段整体压缩老年代。
8.1 为什么不能直接一边运行一边删除
业务线程会持续改变引用:
原来:A -> B,C -X-> B
标记过程中:A -X-> B,C -> B如果GC只看旧快照,可能把仍被C引用的B误判为垃圾。误回收活对象会破坏JVM正确性。因此CMS需要短STW建立起点、并发标记、写屏障记录变化,再STW重新标记修正遗漏。
8.2 主要阶段
flowchart TD
A["Initial Mark:STW"] --> B["Concurrent Mark:与业务并发"]
B --> C["Concurrent Preclean等辅助阶段"]
C --> D["Remark:STW修正并发变化"]
D --> E["Concurrent Sweep:与业务并发清除"]
E --> F["Concurrent Reset:准备下一周期"]初始标记
短暂停业务线程,标记GC Roots直接关联的对象,并建立并发标记起点。它不是完成整个老年代遍历。
并发标记
GC线程从已发现对象继续遍历对象图,业务线程同时运行。该阶段通常耗时较长,但大部分不是STW。GC线程会占用CPU,业务延迟仍可能受资源竞争影响。
预清理与可中止预清理
CMS会尝试处理并发期间的引用变化,减少Remark工作量。具体阶段是否出现、名称和行为受版本与参数影响,排查以实际GC日志为准。
重新标记
再次STW,处理并发标记期间业务线程造成的对象图变化,形成可安全清除的最终标记结果。Remark并不一定很短;对象图复杂、脏卡多、年轻代状态和CPU不足都可能放大停顿。
并发清除
业务线程继续运行,CMS回收被判定不可达的老年代块。线程数不是永远固定为一条,应由实现、版本和参数决定,不能把教材示意写成普遍事实。
重置
清理内部状态,为下一次CMS周期做准备。
8.3 Card Table与写屏障
Young GC若每次扫描整个老年代来找“老对象引用年轻对象”,暂停会很长。HotSpot把堆划成较小Card,并用Card Table记录哪些Card可能包含跨代引用。
业务线程执行引用写入时,JIT插入写屏障,大致完成:
对象字段 = 新引用
把对应Card标记为dirtyYoung GC只需扫描GC Roots和脏Card相关区域,而不是整个老年代。写屏障不是Java锁,而是JVM/JIT插入的额外记账逻辑;它会有少量运行时成本,但换来更短的收集扫描范围。
8.4 浮动垃圾
并发标记完成后,业务线程还会继续产生新的不可达对象。这些对象可能错过本轮判定,只能留到下一轮回收,称为浮动垃圾。因此CMS必须在老年代完全塞满前提前启动并保留并发周期余量。
8.5 Concurrent Mode Failure
如果CMS并发回收尚未完成,业务分配或年轻代晋升已经耗尽可用老年代空间,JVM可能退化到更重的STW收集,形成长暂停。
flowchart TD
A["老年代增长"] --> B["CMS并发周期启动"]
B --> C["业务继续分配与晋升"]
C --> D{"回收速度赶得上吗"}
D -- "是" --> E["释放空间并完成周期"]
D -- "否" --> F["Concurrent Mode Failure"]
F --> G["退化为重型STW回收"]
G --> H{"仍能获得空间吗"}
H -- "不能" --> I["OutOfMemoryError"]原因可能是启动过晚、晋升突增、CPU被业务抢占、碎片、大对象或堆容量不足。不能只靠提前启动百分比掩盖泄漏和无界队列。
8.6 碎片为什么是CMS核心代价
普通CMS并发清除把垃圾块加入空闲列表,不移动所有存活对象。总空闲空间足够时,也可能因没有足够大的连续块而分配失败。CMS可在Full GC等路径进行压缩,但压缩需要移动对象和更新引用,通常会带来长STW。
JDK 7/8历史参数包括:
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0参数可用性和默认值必须用目标JDK验证。CMS已经退出新版本主线,新项目不应为追求“熟悉”而回退到CMS。
九、G1 Region模型
9.1 G1不是取消分代
G1把堆切成大小相等的Region,Region在运行时可承担Eden、Survivor、Old或Humongous等角色。物理上不要求整个年轻代和老年代各自连续,但逻辑分代依然存在。
flowchart TD
A["整个Java堆"] --> B["多个等大小Region"]
B --> C["部分Region作为Eden"]
B --> D["部分Region作为Survivor"]
B --> E["部分Region作为Old"]
B --> F["连续Region容纳Humongous对象"]Region化让G1可以选择一部分回收收益高的区域组成Collection Set,而不必每次都处理整个老年代。
9.2 Young GC
Eden Region分配压力达到条件后:
- 业务线程STW。
- 从Roots和相关Remembered Set扫描存活对象。
- 将存活对象并行复制到新的Survivor或Old Region。
- 回收原Collection Set中的Region。
- 更新引用并恢复业务线程。
因此G1 Young GC也是Evacuation Pause,并不是并发复制。
9.3 并发标记与SATB
G1使用SATB(Snapshot At The Beginning)思想维护并发标记的逻辑快照。业务线程覆盖旧引用前,写屏障把需要保留的旧引用记录到队列,避免本应属于起始快照的对象因引用删除而漏标。
可以把它简化理解为:
old = object.field
记录old引用
object.field = newValue真实实现涉及线程本地队列、并发处理和多种优化,不是每次写引用都直接加全局锁。
9.4 Remembered Set解决跨Region扫描
如果Region A中的对象引用Region B,回收B时不能只扫描B本身。G1为Region维护跨Region引用摘要,底层结合Card Table、写屏障和Remembered Set。
需要分清:
- Card是堆地址范围的细粒度记账单位。
- Card Table记录Card是否可能被修改或包含需要关注的引用。
- Remembered Set从被引用Region角度记录“哪些外部Card可能指向我”。
- 它们减少全堆扫描,但会消耗额外内存、CPU和写屏障成本。
9.5 Mixed GC
并发标记完成后,G1知道各Old Region的存活率和回收收益。后续Mixed GC通常在STW中同时回收:
- 所有或主要年轻Region。
- 一部分垃圾比例高、预计收益好的Old Region。
G1会基于历史成本模型和暂停目标选择Collection Set。MaxGCPauseMillis仍然只是软目标;存活对象过多、RSet扫描量大、Humongous对象多或CPU不足时,实际暂停仍可能超标。
9.6 Humongous对象
通常超过单个Region一半的对象被视为Humongous,需要一个或多个连续Region。大数组、超大响应体和一次性读入文件会导致:
- 需要连续Region,增加分配失败风险。
- 最后一个Region产生尾部浪费。
- 频繁大对象分配提前触发标记或GC。
- 堆看似还有空闲Region,但连续性或回收时机不满足。
解决方向应优先是流式处理、分块、限制批量大小和避免不必要复制,而不是只调大堆。
9.7 To-space exhausted与Full GC
Evacuation需要目标Region容纳复制后的存活对象。如果目标空间不足,日志可能出现To-space exhausted或Evacuation Failure,导致对象原地保留、额外修复成本,甚至最终Full GC。
常见原因:
- 堆太满,空Region储备不足。
- 存活率突然升高。
- 晋升/复制量突增。
- Humongous对象占用大量Region。
- 并发标记启动太晚或回收赶不上分配。
十、G1主要周期怎么串起来
flowchart TD
A["业务在Eden Region分配"] --> B["Young GC复制存活对象"]
B --> C{"老年代占用达到并发标记条件"}
C -- "否" --> A
C -- "是" --> D["Initial Mark通常借Young GC完成"]
D --> E["Concurrent Mark"]
E --> F["Remark:STW"]
F --> G["Cleanup:统计Region收益"]
G --> H["若干次Mixed GC"]
H --> A
H --> I{"正常回收无法满足分配"}
I -- "是" --> J["退化/Full GC/最终OOM"]G1日志中的Pause Young、Concurrent Mark Cycle、Pause Remark、Pause Cleanup、Pause Young (Mixed)不是互不相关的孤立事件,要按时间线看完整周期。
十一、怎样选择收集器
11.1 先问业务,不先问参数
| 问题 | 为什么重要 |
|---|---|
| 可接受P99/P999暂停是多少 | 决定是否需要并发低停顿方案 |
| 堆是256MiB、4GiB还是100GiB | 堆规模改变扫描、复制和整理成本 |
| CPU核数和容器quota是多少 | 并行/并发线程需要真实CPU预算 |
| 分配率和存活率怎样 | 决定Young频率和复制/晋升成本 |
| 是否有大数组和突发批量 | 影响Humongous、晋升和峰值 |
| 更看重单次延迟还是总完成时间 | 区分在线服务与批处理 |
| 使用哪个JDK完整版本 | 决定收集器、参数和Bug修复范围 |
11.2 典型起点
- JDK 7/8小堆、单CPU或简单工具:评估Serial。
- JDK 7/8批处理、吞吐优先:评估Parallel系列。
- JDK 7/8在线大堆历史系统:CMS或G1都可能存在,应基于现状、版本和压测评估;不要只因CMS“低停顿”就切换。
- JDK 9+普通服务:通常先从默认G1出发,再用日志和SLO证明是否需要改变。
- 新JDK超大堆、极低停顿:可评估ZGC/Shenandoah,但必须验证版本、发行版、吞吐、CPU和运维能力。
收集器切换会改变日志、参数、内存布局、停顿形态和故障模式,属于需要压测、灰度和回滚方案的生产变更。
十二、JDK7/8可运行Demo
下面代码只使用JDK 7语法/API:大量临时对象制造分配率,survivors保留部分数组制造存活与晋升压力。
import java.util.ArrayList;
import java.util.List;
public class GcCollectorDemo {
public static void main(String[] args) throws Exception {
List<byte[]> survivors = new ArrayList<byte[]>();
for (int round = 0; round < 80; round++) {
for (int i = 0; i < 160; i++) {
byte[] temporary = new byte[32 * 1024];
temporary[0] = (byte) i;
}
if (round % 8 == 0) {
survivors.add(new byte[512 * 1024]);
}
if (survivors.size() > 6) {
survivors.remove(0);
}
Thread.sleep(20L);
}
System.out.println("survivors=" + survivors.size());
}
}编译:
javac GcCollectorDemo.java12.1 JDK7/8 Serial日志
java -Xms32m -Xmx32m \
-XX:+UseSerialGC \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintTenuringDistribution \
-Xloggc:serial-gc.log \
GcCollectorDemo12.2 JDK7/8 Parallel日志
java -Xms32m -Xmx32m \
-XX:+UseParallelGC \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintTenuringDistribution \
-Xloggc:parallel-gc.log \
GcCollectorDemo12.3 JDK8 G1日志
java -Xms32m -Xmx32m \
-XX:+UseG1GC \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintAdaptiveSizePolicy \
-Xloggc:g1-gc.log \
GcCollectorDemo不同8u更新版支持的细分日志参数可能不同,若启动时报Unrecognized VM option,先执行:
java -XX:+PrintFlagsFinal -version并以目标发行版文档和实际Flags为准,不要直接删除所有日志参数继续上线。
12.4 JDK9+统一日志
JDK 9+不能把下面写法反向复制到JDK 8:
java -Xms32m -Xmx32m \
-XX:+UseG1GC \
-Xlog:gc*,gc+age=trace:file=g1-gc.log:time,uptime,level,tags \
GcCollectorDemo12.5 实验应比较什么
不要只比较“日志行数”:
- Young GC频率和每次暂停。
- GC前后年轻代下降量。
- 老年代基线是否上升。
- 存活对象复制/晋升量。
- 总运行时间与GC时间占比。
- 是否出现Full GC、退化或分配失败。
- 同一堆、同一代码、预热后多次运行是否稳定。
这个小Demo只能帮助认识日志,不足以给生产系统选型。真实选型必须用真实流量模型、对象大小、存活率和CPU限制压测。
十三、商业场景:订单服务停顿突然升高
13.1 现象
JDK 8订单服务使用CMS,平时P99为80ms,促销开始后每隔几分钟出现5秒暂停,日志出现Concurrent Mode Failure。
13.2 不完整结论
CMS不行,直接换G1。这个结论没有解释为什么促销前正常,也没有证明G1能解决对象增长。
13.3 正确证据链
- 从发布平台确认JDK完整版本、启动参数、容器limit和本次代码版本。
- 从APM确认暂停时间与接口P99、错误率的时间对齐。
- 从GC日志确认CMS启动、Remark、Concurrent Mode Failure和Full GC时间线。
- 计算老年代GC后基线、晋升速率、分配速率和CMS周期耗时。
- 检查容器CPU throttling;CMS线程被限速会延长并发周期。
- 用对象直方图和Heap Dump判断是订单缓存无界增长、批量结果保留,还是正常容量峰值。
- 检查大数组和碎片,而不是只看“老年代还有多少总空闲”。
flowchart TD
A["长暂停告警"] --> B["对齐GC日志与业务时间"]
B --> C{"Concurrent Mode Failure?"}
C -- "是" --> D["检查CMS启动时机与并发周期"]
D --> E["检查晋升、分配率与CPU throttling"]
E --> F["Histogram/Dump判断容量还是泄漏"]
F --> G["先修无界持有或突发分配"]
G --> H["再压测参数或收集器变更"]13.4 修复可能性
- 修复无界本地缓存,增加容量、TTL和监控。
- 批量查询改分页/流式,减少大数组和短时晋升洪峰。
- 给并发GC保留真实CPU,不让容器quota与GC线程配置严重冲突。
- 在证据证明启动过晚时调整CMS启动策略,而不是掩盖泄漏。
- 评估升级到受支持JDK并迁移G1;用同一流量压测P99、吞吐、CPU、内存余量和回退路径。
十四、生产排查Runbook
14.1 先确认当前到底用什么GC
java -XX:+PrintCommandLineFlags -version
jcmd <pid> VM.flags
jcmd <pid> VM.command_line不要根据“JDK8默认”猜测。
14.2 保留时间线
收集:
- GC日志原文件及其时区。
- 应用错误日志、请求P99和错误率。
- CPU、cgroup throttling、RSS、Heap/Old使用率。
- 分配率、晋升率、线程数。
- 发布、扩容和流量事件。
14.3 根据收集器看对应证据
| 收集器 | 重点日志/现象 |
|---|---|
| Serial/Parallel | Young/Full频率、存活量、晋升、长STW、回收后基线 |
| CMS | Initial Mark、Remark、周期耗时、浮动垃圾余量、Promotion Failed、Concurrent Mode Failure、碎片 |
| G1 | Evacuation Pause、RSet/Ext Root扫描、Concurrent Cycle、Mixed、Humongous、To-space exhausted、Full GC |
14.4 判断是哪类问题
GC后占用能回落,但很快再次涨满
→ 容量/分配率/突发流量问题优先
GC后老年代基线持续抬升
→ 长寿命对象增长或泄漏优先
占用不高但暂停长
→ 存活对象复制、RSet扫描、CPU限速、安全点等方向
并发周期未完成就耗尽空间
→ 启动时机、CPU、晋升洪峰、碎片、容量或泄漏
大数组频繁出现
→ Humongous/连续空间/批量与复制链路14.5 变更闭环
任何GC参数或收集器变更都应:
- 固定JDK和镜像Digest。
- 建立变更前基线。
- 使用代表性流量压测。
- 同时比较P99/P999、吞吐、CPU、RSS和错误率。
- 金丝雀发布并设置硬回滚条件。
- 保留旧参数和旧镜像回退能力。
十五、常见误区
| 误区 | 正确理解 |
|---|---|
| G1没有年轻代和老年代 | G1物理Region不要求代连续,但逻辑分代仍存在 |
| 并发GC没有STW | CMS和G1都包含STW阶段 |
| Parallel是低停顿收集器 | 它主要优化总体吞吐 |
| CMS不会Full GC | 并发失败、碎片和其他条件都可能触发重型STW |
| G1不会产生碎片 | Region内复制整理减少外部碎片,但Humongous和Region利用仍有问题 |
MaxGCPauseMillis=200保证不超过200ms | 它是软目标,不能替代业务SLO验证 |
| Full GC次数为0就一定健康 | Young暂停、并发CPU、分配率和业务延迟仍可能异常 |
| 换成G1就能修内存泄漏 | 收集器不能回收仍被GC Roots强引用的对象 |
| JDK8都支持相同参数 | 8u更新、发行版和构建差异必须验证 |
| GC线程越多越快 | 协调、CPU quota和业务竞争可能让整体更慢 |
十六、面试标准回答
Serial、Parallel、CMS、G1怎样比较
Serial在STW期间主要单线程回收,适合小堆和少CPU;Parallel系列在STW期间多线程回收,以总体吞吐为主要目标;CMS让老年代标记和清除的大部分阶段与业务并发,降低停顿但会产生浮动垃圾、CPU竞争和碎片,并可能Concurrent Mode Failure;G1把堆划成Region并保留逻辑分代,通过并发标记、Remembered Set和收益预测选择Collection Set,支持Young和Mixed GC,但仍有STW、写屏障成本、Humongous和Evacuation失败风险。没有绝对最优,必须按JDK版本、堆大小、CPU和延迟SLO压测。
G1为什么还有新生代和老年代
G1取消的是“年轻代和老年代必须各自占一整段连续地址”的物理布局,不是取消分代。Region会动态承担Eden、Survivor、Old和Humongous角色,Young GC回收年轻Region,Mixed GC再加入部分收益高的Old Region。
CMS为什么会Concurrent Mode Failure
CMS并发回收期间业务仍在分配并发生晋升。如果并发周期完成前老年代可用空间已经不足,就无法继续满足分配,可能退化到长时间STW回收。根因可能是启动过晚、晋升突增、CPU不足、碎片、容量不足或泄漏,不能只靠调低触发阈值下结论。
Card Table和Remembered Set解决什么
它们用于缩小跨代或跨Region引用的扫描范围。业务写引用时,JIT写屏障把相关Card记脏;Young GC或G1回收某Region时,扫描Roots和相关脏Card/RSet,而不是遍历整个老年代或整个堆。代价是写屏障CPU、队列和元数据内存。
JDK7/8和JDK9+日志参数有什么区别
JDK7/8常用
PrintGCDetails、PrintGCDateStamps和-Xloggc;JDK9+使用统一日志-Xlog:gc*。不能把-Xlog复制到JDK8。CMS在JDK7/8常用,JDK9废弃、JDK14移除;G1在JDK8可用,JDK9起成为HotSpot常见默认。
十七、关联知识点
十八、学习验收
如果真正理解了本页,应能完成:
- [ ] 不看资料画出CMS主要阶段并解释两次STW的必要性。
- [ ] 解释并行回收为什么仍然可能是STW。
- [ ] 解释G1 Region化与逻辑分代为什么不矛盾。
- [ ] 从JDK8 GC日志区分Young GC、CMS周期和Full GC。
- [ ] 解释浮动垃圾、碎片和Concurrent Mode Failure之间的关系。
- [ ] 解释Card Table、写屏障、Remembered Set和SATB各自解决的问题。
- [ ] 说明JDK7/8与JDK9+收集器、默认值和日志参数边界。
- [ ] 用同一Demo比较Serial、Parallel和G1,而不是只比较某一次暂停。
- [ ] 面对长暂停先建立证据链,再决定调参数、改代码、扩容或换收集器。
本章小结
HotSpot收集器的本质不是名称列表,而是不同的并发控制、对象图维护和空间回收策略。Serial用简单换低开销,Parallel用集中并行换吞吐,CMS用并发标记清除换较短停顿但承担碎片和并发失败,G1用Region、记忆集、并发标记和收益预测换可控停顿目标。真正的生产能力,是知道每种方案为什么有效、在哪些条件下失效,并能从GC日志、资源指标和对象证据证明结论。
