Skip to content

HotSpot垃圾收集器完整原理与JDK7/8选型

垃圾收集器不是“自动删除没用对象”的一个后台线程,而是一套同时解决以下问题的运行时系统:

  1. 怎样从GC Roots判断对象是否仍然存活。
  2. 怎样在业务线程不断修改引用时得到可信的对象图。
  3. 怎样回收空间、整理碎片并更新对象引用。
  4. 怎样在吞吐量、暂停时间、内存占用和CPU开销之间取舍。
  5. 常规回收赶不上分配速度时,怎样退化、失败或最终抛出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。
mermaid
flowchart TD
    A["GC周期开始"] --> B["短暂STW建立可靠起点"]
    B --> C["并发阶段:GC与业务共同运行"]
    C --> D["短暂STW修正并发期间变化"]
    D --> E["回收或转移对象"]
    E --> F["业务继续运行"]

二、为什么不存在适合所有系统的收集器

收集器要同时面对四个互相牵制的目标:

目标含义典型业务
吞吐量用户代码时间占总时间比例尽量高离线计算、报表、批处理
暂停时间单次STW尽量短且P99可控网关、支付、在线接口
内存占用GC辅助结构、预留空间尽量少小容器、边缘节点
CPU成本GC不长期抢占业务CPUCPU紧张或高密度部署

降低暂停常常需要更频繁回收、更多并发线程、更多Region/记忆集元数据和更大的空闲余量;提高吞吐常常允许更长的集中暂停。选型必须先明确业务SLO,不能只问“哪个GC最快”。

三、JDK7、JDK8与JDK9+版本地图

收集器JDK 7/8地位后续变化典型定位
Serial / Serial Old可用仍可用小堆、单核、工具进程
ParNew常与CMS搭配JDK 9起组合受限,后续逐渐退出主线CMS年轻代伙伴
Parallel Scavenge / Parallel OldJDK 7/8服务端常见默认组合,但必须以实际Ergonomics为准JDK 9默认不再是它吞吐优先
CMSJDK 7/8低停顿常用方案JDK 9废弃,JDK 14移除历史低停顿方案
G1JDK 7u4引入,JDK 8逐步成熟;不是JDK 8所有发行版默认JDK 9起HotSpot常见默认大堆、可预测停顿目标
ZGCJDK 7/8不存在JDK 11引入,后续持续演进超大堆、极低停顿
Shenandoah主流Oracle JDK 7/8没有不同OpenJDK发行版和版本支持不同并发转移、低停顿

“JDK 8默认一定是Parallel”也不能脱离上下文绝对化。默认由Server/Client VM、CPU、内存、操作系统、发行版和启动参数共同决定。生产应直接查看:

bash
java -version
java -XX:+PrintCommandLineFlags -version

运行中进程可使用:

bash
jcmd <pid> VM.flags
jcmd <pid> VM.command_line

JDK 7早期更新版的工具能力和参数支持可能不同,必须记录完整版本,例如1.7.0_801.8.0_202,不能只记录“JDK8”。

四、收集器组合为什么重要

JDK 7/8经典分代收集器通常要同时选择年轻代和老年代方案:

mermaid
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年轻代收集器通常使用复制思想:

mermaid
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容易变长。

启用:

bash
-XX:+UseSerialGC

它通常同时选择Serial Young和Serial Old。不要把“Serial适合客户端”理解成只能在桌面程序使用;小型命令行工具、测试任务和极小容器也可能适合,最终以停顿和吞吐实测为准。

5.2 Serial Old过程

Serial Old面向老年代,典型思想是标记—整理:

  1. STW后从GC Roots做可达性分析。
  2. 标记存活对象。
  3. 移动并压紧存活对象,消除外部碎片。
  4. 更新所有受影响引用。
  5. 恢复业务线程。

它还可能成为CMS并发失败时的退化后备路径,所以使用CMS并不代表永远不会见到Serial Old式长暂停。

六、ParNew

ParNew可以理解为Serial年轻代收集逻辑的并行版本:业务线程STW后,多条GC工作线程并行扫描和复制存活对象。

bash
-XX:+UseParNewGC

JDK 7/8中它最重要的历史价值是与CMS搭配。它并不是“多线程就一定比Serial快”:

  • 小堆、单核或CPU受限时,线程协调成本可能得不偿失。
  • GC线程数过高会争抢CPU。
  • 存活对象很多时,复制和晋升成本仍然高。
  • 老年代压力不会因为年轻代并行就消失。

JDK 8可用-XX:ParallelGCThreads影响STW并行GC线程数量,但生产不应脱离CPU配额盲目设置。

七、Parallel Scavenge与Parallel Old

7.1 目标是总体吞吐,不是每次暂停最短

吞吐量定义为:

text
吞吐量 = 业务线程运行时间 / (业务线程运行时间 + GC时间)

Parallel Scavenge在年轻代并行复制,Parallel Old在老年代并行标记整理。启用组合:

bash
-XX:+UseParallelGC

JDK 8中通常会配合Parallel Old;具体选择仍应查看启动日志和Flags。

7.2 三个容易写错或误解的参数

bash
-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 为什么不能直接一边运行一边删除

业务线程会持续改变引用:

text
原来:A -> B,C -X-> B
标记过程中:A -X-> B,C -> B

如果GC只看旧快照,可能把仍被C引用的B误判为垃圾。误回收活对象会破坏JVM正确性。因此CMS需要短STW建立起点、并发标记、写屏障记录变化,再STW重新标记修正遗漏。

8.2 主要阶段

mermaid
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插入写屏障,大致完成:

text
对象字段 = 新引用
把对应Card标记为dirty

Young GC只需扫描GC Roots和脏Card相关区域,而不是整个老年代。写屏障不是Java锁,而是JVM/JIT插入的额外记账逻辑;它会有少量运行时成本,但换来更短的收集扫描范围。

8.4 浮动垃圾

并发标记完成后,业务线程还会继续产生新的不可达对象。这些对象可能错过本轮判定,只能留到下一轮回收,称为浮动垃圾。因此CMS必须在老年代完全塞满前提前启动并保留并发周期余量。

8.5 Concurrent Mode Failure

如果CMS并发回收尚未完成,业务分配或年轻代晋升已经耗尽可用老年代空间,JVM可能退化到更重的STW收集,形成长暂停。

mermaid
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历史参数包括:

bash
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0

参数可用性和默认值必须用目标JDK验证。CMS已经退出新版本主线,新项目不应为追求“熟悉”而回退到CMS。

九、G1 Region模型

9.1 G1不是取消分代

G1把堆切成大小相等的Region,Region在运行时可承担Eden、Survivor、Old或Humongous等角色。物理上不要求整个年轻代和老年代各自连续,但逻辑分代依然存在。

mermaid
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分配压力达到条件后:

  1. 业务线程STW。
  2. 从Roots和相关Remembered Set扫描存活对象。
  3. 将存活对象并行复制到新的Survivor或Old Region。
  4. 回收原Collection Set中的Region。
  5. 更新引用并恢复业务线程。

因此G1 Young GC也是Evacuation Pause,并不是并发复制。

9.3 并发标记与SATB

G1使用SATB(Snapshot At The Beginning)思想维护并发标记的逻辑快照。业务线程覆盖旧引用前,写屏障把需要保留的旧引用记录到队列,避免本应属于起始快照的对象因引用删除而漏标。

可以把它简化理解为:

text
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主要周期怎么串起来

mermaid
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保留部分数组制造存活与晋升压力。

java
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());
    }
}

编译:

bash
javac GcCollectorDemo.java

12.1 JDK7/8 Serial日志

bash
java -Xms32m -Xmx32m \
  -XX:+UseSerialGC \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintTenuringDistribution \
  -Xloggc:serial-gc.log \
  GcCollectorDemo

12.2 JDK7/8 Parallel日志

bash
java -Xms32m -Xmx32m \
  -XX:+UseParallelGC \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintTenuringDistribution \
  -Xloggc:parallel-gc.log \
  GcCollectorDemo

12.3 JDK8 G1日志

bash
java -Xms32m -Xmx32m \
  -XX:+UseG1GC \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintAdaptiveSizePolicy \
  -Xloggc:g1-gc.log \
  GcCollectorDemo

不同8u更新版支持的细分日志参数可能不同,若启动时报Unrecognized VM option,先执行:

bash
java -XX:+PrintFlagsFinal -version

并以目标发行版文档和实际Flags为准,不要直接删除所有日志参数继续上线。

12.4 JDK9+统一日志

JDK 9+不能把下面写法反向复制到JDK 8:

bash
java -Xms32m -Xmx32m \
  -XX:+UseG1GC \
  -Xlog:gc*,gc+age=trace:file=g1-gc.log:time,uptime,level,tags \
  GcCollectorDemo

12.5 实验应比较什么

不要只比较“日志行数”:

  1. Young GC频率和每次暂停。
  2. GC前后年轻代下降量。
  3. 老年代基线是否上升。
  4. 存活对象复制/晋升量。
  5. 总运行时间与GC时间占比。
  6. 是否出现Full GC、退化或分配失败。
  7. 同一堆、同一代码、预热后多次运行是否稳定。

这个小Demo只能帮助认识日志,不足以给生产系统选型。真实选型必须用真实流量模型、对象大小、存活率和CPU限制压测。

十三、商业场景:订单服务停顿突然升高

13.1 现象

JDK 8订单服务使用CMS,平时P99为80ms,促销开始后每隔几分钟出现5秒暂停,日志出现Concurrent Mode Failure。

13.2 不完整结论

text
CMS不行,直接换G1。

这个结论没有解释为什么促销前正常,也没有证明G1能解决对象增长。

13.3 正确证据链

  1. 从发布平台确认JDK完整版本、启动参数、容器limit和本次代码版本。
  2. 从APM确认暂停时间与接口P99、错误率的时间对齐。
  3. 从GC日志确认CMS启动、Remark、Concurrent Mode Failure和Full GC时间线。
  4. 计算老年代GC后基线、晋升速率、分配速率和CMS周期耗时。
  5. 检查容器CPU throttling;CMS线程被限速会延长并发周期。
  6. 用对象直方图和Heap Dump判断是订单缓存无界增长、批量结果保留,还是正常容量峰值。
  7. 检查大数组和碎片,而不是只看“老年代还有多少总空闲”。
mermaid
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

bash
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/ParallelYoung/Full频率、存活量、晋升、长STW、回收后基线
CMSInitial Mark、Remark、周期耗时、浮动垃圾余量、Promotion Failed、Concurrent Mode Failure、碎片
G1Evacuation Pause、RSet/Ext Root扫描、Concurrent Cycle、Mixed、Humongous、To-space exhausted、Full GC

14.4 判断是哪类问题

text
GC后占用能回落,但很快再次涨满
→ 容量/分配率/突发流量问题优先

GC后老年代基线持续抬升
→ 长寿命对象增长或泄漏优先

占用不高但暂停长
→ 存活对象复制、RSet扫描、CPU限速、安全点等方向

并发周期未完成就耗尽空间
→ 启动时机、CPU、晋升洪峰、碎片、容量或泄漏

大数组频繁出现
→ Humongous/连续空间/批量与复制链路

14.5 变更闭环

任何GC参数或收集器变更都应:

  1. 固定JDK和镜像Digest。
  2. 建立变更前基线。
  3. 使用代表性流量压测。
  4. 同时比较P99/P999、吞吐、CPU、RSS和错误率。
  5. 金丝雀发布并设置硬回滚条件。
  6. 保留旧参数和旧镜像回退能力。

十五、常见误区

误区正确理解
G1没有年轻代和老年代G1物理Region不要求代连续,但逻辑分代仍存在
并发GC没有STWCMS和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常用PrintGCDetailsPrintGCDateStamps-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日志、资源指标和对象证据证明结论。