JVM GC 图解:从入门到真正理解垃圾回收

垃圾回收(Garbage Collection)是 JVM 最核心的运行时机制,也是 Java 性能调优的关键知识点。很多人用了多年 Java,却对 GC 的工作原理一知半解——知道有 Minor GC 和 Full GC,知道要避免频繁 Full GC,但说不清楚 G1 和 CMS 有什么本质区别,也不知道为什么 GC 会让应用暂停。

本文从最基础的内存结构开始,用大量图示一步步拆解 GC 的每个环节,力求让每个概念都有直觉上的理解。

一、JVM 堆内存结构

GC 管理的核心区域是堆(Heap)。JVM 启动时会申请一块连续内存作为堆,所有 new 出来的对象都在这里分配。

graph LR
    subgraph Young[新生代 Young Gen]
        Eden[Eden 区 ~80%]
        S0[S0 From ~10%]
        S1[S1 To ~10%]
    end
    subgraph Old[老年代 Old Gen]
        OldObj[长期存活对象 / 大对象]
    end
    subgraph NonHeap[非堆 不由GC管理]
        Meta[Metaspace 类定义/常量]
        Stack[虚拟机栈/PC寄存器]
    end

    Eden -->|Minor GC 复制| S1
    S0 -->|Minor GC 年龄+1| S1
    S1 -->|年龄达阈值/大对象| OldObj

新生代分三个区:

  • Eden 区:新对象出生的地方,占新生代约 80%
  • Survivor 0(From):上一次 Minor GC 后存活对象的暂居地
  • Survivor 1(To):本次 Minor GC 时存活对象的目标地

这个设计来自一个重要的经验观察——分代假说

二、分代假说:GC 设计的理论基础

JVM 内存分代设计基于两个经验规律:

弱分代假说:绝大多数对象都是"朝生夕死"的,极短命。
强分代假说:熬过多次 GC 的对象,未来也不太可能死。

用数据说话:实际 Java 程序中,一次 Minor GC 通常能回收 95%+ 的新生代对象。也就是说,大多数对象在 Eden 区被创建,在第一次 GC 时就死亡了,根本没有机会进入 Survivor 区。

对象存活率随年龄的变化(示意):

存活率
100% │
     │  ███
 50% │     ████
     │         ████
 10% │              ████████
  5% │                      █████████████████(趋于稳定)
  0% └─────────────────────────────────────────────────→ GC 轮次
      1次  2次  3次  4次  5次  6次  7次  8次  ...

绝大多数对象在第 1 次 GC 就被回收
少数"长命"对象在多次 GC 后趋于稳定存活

这个规律决定了:

  • 新生代可以用复制算法——只有少数对象需要被复制,效率极高
  • 老年代对象密度高,不适合复制,用标记-整理标记-清除
  • 新生代 GC(Minor GC)可以频繁运行,开销小;老年代 GC(Full GC)代价大,要尽量避免

三、Minor GC:新生代的清理过程

当 Eden 区满了,就触发 Minor GC。整个过程分四步:

第一步:Eden 区满,触发 GC

Minor GC 触发前:Eden 区已满
新生代 Young Gen
Eden(已满 ~80%)
A B C D E F G H I J K
S0 From(~10%)
X1 Y1
S1 To(~10%)
老年代 Old Gen
长期存活对象...
存活对象 死亡(不可达) 存活(年龄1) 存活(年龄2+)

第二步:标记存活对象(可达性分析)

graph TD
    GCRoot1["GC Root (线程栈局部变量)"]
    GCRoot2["GC Root (静态变量 / JNI)"]

    GCRoot1 -->|引用| A["对象 A ✓ 存活"]
    GCRoot1 -->|引用| F["对象 F ✓ 存活"]
    GCRoot2 -->|引用| H["对象 H ✓ 存活"]
    GCRoot2 -->|引用| X["对象 X ✓ 存活"]

    A -->|引用| C["对象 C ✓ 存活"]
    X -->|引用| Y["对象 Y ✓ 存活"]

    B["对象 B ✗ 不可达 = 垃圾"]
    D["对象 D ✗ 不可达 = 垃圾"]
    E["对象 E ✗ 不可达 = 垃圾"]
    G["对象 G ✗ 不可达 = 垃圾"]

    style A fill:#d4edda,stroke:#28a745
    style C fill:#d4edda,stroke:#28a745
    style F fill:#d4edda,stroke:#28a745
    style H fill:#d4edda,stroke:#28a745
    style X fill:#d4edda,stroke:#28a745
    style Y fill:#d4edda,stroke:#28a745
    style B fill:#f8d7da,stroke:#dc3545
    style D fill:#f8d7da,stroke:#dc3545
    style E fill:#f8d7da,stroke:#dc3545
    style G fill:#f8d7da,stroke:#dc3545

第三步:复制存活对象到 S1(To 区),年龄 +1

Minor GC 完成后:存活对象复制到 S1,Eden 和 S0 清空
新生代 Young Gen
Eden(已清空)
全部回收
S0(已清空)
全部回收
S1 To(存活对象)
A1 C1 F1 H1 X2 Y2
老年代 Old Gen
不变

Eden 中的存活对象(A C F H)复制到 S1,年龄 0→1;S0 中的存活对象(X Y)复制到 S1,年龄 1→2;Eden 和 S0 整体清空,无需逐个释放。

第四步:S0/S1 角色互换

角色互换后:S1 变为新 From,等待下一次 Minor GC
新生代 Young Gen
Eden(接收新对象)
...
S0 From(原 S1)
A1 C1 F1 H1 X2 Y2
S1 To(原 S0,空)
等待下次 GC
老年代 Old Gen
不变

何时晋升老年代?

两种情况会让对象从新生代晋升到老年代:

1. 年龄阈值(默认 15):
   对象每熬过一次 Minor GC,年龄 +1
   年龄达到 MaxTenuringThreshold(默认 15)时,晋升老年代

   年龄:  1    2    3  ...  15   → 进入老年代
          ↑    ↑    ↑        ↑
        GC1  GC2  GC3      GC15

2. 大对象直接进老年代:
   超过 -XX:PretenureSizeThreshold 的对象直接分配在老年代
   避免大对象在 Eden/Survivor 区来回复制的巨大开销

四、Full GC:最昂贵的操作

Full GC 会回收整个堆(新生代 + 老年代),期间会触发 Stop-The-World(STW)——暂停所有用户线程,直到 GC 完成。

gantt
    title Stop-The-World 示意(Full GC)
    dateFormat  X
    axisFormat %s ms

    section 用户线程
    正常运行          :active, u1, 0, 40
    暂停(STW 期间)  :crit, stw, 40, 80
    恢复运行          :active, u2, 80, 120

    section GC 线程
    等待              :done, g0, 0, 40
    Full GC 执行      :active, gc1, 40, 80
    空闲              :done, g1, 80, 120

STW 期间所有用户请求无响应! Full GC 的停顿时间从几百毫秒到几秒不等,这就是为什么 Full GC 对线上服务是灾难性的。

触发 Full GC 的常见原因:

1. 老年代空间不足
   ┌────────────────────────────────────┐
   │  老年代(95% 已满)                  │  ← 新对象晋升时放不下
   │  ████████████████████████████████  │     → 触发 Full GC
   └────────────────────────────────────┘

2. Minor GC 晋升失败(Promotion Failure)
   老年代剩余空间 < 新生代存活对象总大小
   JVM 无法保证晋升成功 → 触发 Full GC

3. 显式调用 System.gc()(强烈不建议)

4. Metaspace 空间不足(类加载过多)

5. CMS GC 的并发失败(Concurrent Mode Failure)

五、可达性分析:判断对象是否存活

GC 如何知道哪些对象可以回收?答案是可达性分析(Reachability Analysis)。

graph TD
    GCRoots([GC Roots])

    GCRoots -->|引用| A[对象 A 存活]
    GCRoots -->|引用| B[对象 B 存活]
    A -->|引用| C[对象 C 存活]
    A -->|引用| D[对象 D 存活]
    C -->|引用| E[对象 E 存活]

    F[对象 F 不可达]
    G[对象 G 不可达]
    F -->|循环引用| G
    G -->|循环引用| F

    style GCRoots fill:#d1ecf1,stroke:#17a2b8
    style A fill:#d4edda,stroke:#28a745
    style B fill:#d4edda,stroke:#28a745
    style C fill:#d4edda,stroke:#28a745
    style D fill:#d4edda,stroke:#28a745
    style E fill:#d4edda,stroke:#28a745
    style F fill:#f8d7da,stroke:#dc3545
    style G fill:#f8d7da,stroke:#dc3545

GC Roots 包含哪些?

graph TD
    GCRoots([GC Roots])

    R1[① 虚拟机栈局部变量引用 方法内的局部对象]
    R2[② 方法区静态变量引用 static Object shared]
    R3[③ 方法区常量引用 static final 常量]
    R4[④ JNI 本地方法持有的引用]

    GCRoots --> R1
    GCRoots --> R2
    GCRoots --> R3
    GCRoots --> R4

    style GCRoots fill:#d1ecf1,stroke:#17a2b8
    style R1 fill:#f8f9fa,stroke:#6c757d
    style R2 fill:#f8f9fa,stroke:#6c757d
    style R3 fill:#f8f9fa,stroke:#6c757d
    style R4 fill:#f8f9fa,stroke:#6c757d

六、四种引用类型:影响 GC 行为的关键

强度递减 →

强引用(Strong Reference)      弱引用(Weak Reference)
Object obj = new Object();     WeakReference<Object> wr = ...
↓ 只要存在,永远不会被 GC 回收    ↓ 下次 GC 时必然被回收,不管内存是否充足

软引用(Soft Reference)         虚引用(Phantom Reference)
SoftReference<Object> sr = ... PhantomReference<Object> pr = ...
↓ 内存充足时保留,内存不足时回收   ↓ 几乎等于没有引用,专门用于追踪 GC 事件
  (适合实现内存敏感的缓存)
// 软引用:实现 LRU 缓存的经典方案
Map<String, SoftReference<BigImage>> imageCache = new HashMap<>();

// 存入缓存
imageCache.put("logo", new SoftReference<>(loadImage("logo.png")));

// 取出缓存
SoftReference<BigImage> ref = imageCache.get("logo");
BigImage img = ref != null ? ref.get() : null;  // 可能为 null(已被 GC 回收)
if (img == null) {
    img = loadImage("logo.png");  // 重新加载
    imageCache.put("logo", new SoftReference<>(img));
}

// 优点:内存充足时命中缓存(快),内存紧张时 GC 自动清理(不 OOM)

七、三种基础 GC 算法

标记-清除(Mark-Sweep)

graph LR
    subgraph Before[GC 前:内存布局]
        direction LR
        b1["A 活"] --- b2["B 死"] --- b3["C 活"] --- b4["D 死"] --- b5["E 死"] --- b6["F 活"] --- b7["G 死"] --- b8["H 活"] --- b9["I 死"] --- b10["J 活"]
    end

    Before -->|"① 标记存活对象 ② 清除未标记对象"| After

    subgraph After[GC 后:产生内存碎片]
        direction LR
        a1["A ✓"] --- a2["空洞"] --- a3["C ✓"] --- a4["空洞"] --- a5["空洞"] --- a6["F ✓"] --- a7["空洞"] --- a8["H ✓"] --- a9["空洞"] --- a10["J ✓"]
    end

    warn["⚠ 问题:大量不连续小空洞 分配大对象时找不到足够连续空间"]

    style b2 fill:#f8d7da,stroke:#dc3545
    style b4 fill:#f8d7da,stroke:#dc3545
    style b5 fill:#f8d7da,stroke:#dc3545
    style b7 fill:#f8d7da,stroke:#dc3545
    style b9 fill:#f8d7da,stroke:#dc3545
    style a1 fill:#d4edda,stroke:#28a745
    style a3 fill:#d4edda,stroke:#28a745
    style a6 fill:#d4edda,stroke:#28a745
    style a8 fill:#d4edda,stroke:#28a745
    style a10 fill:#d4edda,stroke:#28a745
    style a2 fill:#e2e3e5,stroke:#6c757d
    style a4 fill:#e2e3e5,stroke:#6c757d
    style a5 fill:#e2e3e5,stroke:#6c757d
    style a7 fill:#e2e3e5,stroke:#6c757d
    style a9 fill:#e2e3e5,stroke:#6c757d
    style warn fill:#fff3cd,stroke:#ffc107

标记-复制(Mark-Copy)

graph LR
    subgraph GCBefore[GC 前]
        direction LR
        subgraph From[From 半区(有对象)]
            f1["A 活"] --- f2["B 死"] --- f3["C 活"] --- f4["D 死"] --- f5["E 活"] --- f6["F 死"] --- f7["G 活"]
        end
        subgraph To0[To 半区(空)]
            t0["(空)"]
        end
    end

    GCBefore -->|"复制存活对象到 To From 整体清空"| GCAfter

    subgraph GCAfter[GC 后]
        direction LR
        subgraph From2[From 半区(清空)]
            f8["(空)"]
        end
        subgraph To2[To 半区(紧凑排列,无碎片)]
            t1["A ✓"] --- t2["C ✓"] --- t3["E ✓"] --- t4["G ✓"]
        end
    end

    note["优点:无碎片,分配极快(移动指针) 缺点:内存利用率仅 50% 新生代优化:Eden 80% + S0 10% → S1 10% 实际利用率 90%,只浪费 10%"]

    style f2 fill:#f8d7da,stroke:#dc3545
    style f4 fill:#f8d7da,stroke:#dc3545
    style f6 fill:#f8d7da,stroke:#dc3545
    style t1 fill:#d4edda,stroke:#28a745
    style t2 fill:#d4edda,stroke:#28a745
    style t3 fill:#d4edda,stroke:#28a745
    style t4 fill:#d4edda,stroke:#28a745
    style note fill:#d1ecf1,stroke:#17a2b8

标记-整理(Mark-Compact)

graph LR
    subgraph Before[GC 前:存活与死亡对象交错]
        direction LR
        b1["A 活"] --- b2["B 死"] --- b3["C 活"] --- b4["D 死"] --- b5["E 死"] --- b6["F 活"] --- b7["G 死"] --- b8["H 活"]
    end

    Before -->|"① 标记存活对象 ② 向一端移动整理"| After

    subgraph After[GC 后:存活对象紧凑排列 + 连续空闲空间]
        direction LR
        a1["A ✓"] --- a2["C ✓"] --- a3["F ✓"] --- a4["H ✓"] --- a5["空闲(连续大块)"]
    end

    note["优点:无碎片,内存利用率 100% 缺点:移动对象需更新所有引用指针,开销大 适合老年代:存活率高,偶尔整理可接受"]

    style b2 fill:#f8d7da,stroke:#dc3545
    style b4 fill:#f8d7da,stroke:#dc3545
    style b5 fill:#f8d7da,stroke:#dc3545
    style b7 fill:#f8d7da,stroke:#dc3545
    style a1 fill:#d4edda,stroke:#28a745
    style a2 fill:#d4edda,stroke:#28a745
    style a3 fill:#d4edda,stroke:#28a745
    style a4 fill:#d4edda,stroke:#28a745
    style a5 fill:#e2e3e5,stroke:#6c757d
    style note fill:#d1ecf1,stroke:#17a2b8

八、五大主流垃圾收集器

Serial GC:单线程,最简单

特点:GC 时只用一个线程,STW 期间用户线程全部暂停。适用于客户端应用、单核 CPU、几十到几百 MB 堆。启用:-XX:+UseSerialGC

gantt
    title Serial GC 时间轴
    dateFormat  X
    axisFormat %s

    section 用户线程
    正常运行        :active, u1, 0, 30
    暂停(STW)     :crit, stw1, 30, 70
    恢复运行        :active, u2, 70, 100

    section GC 线程(单线程)
    等待            :done, g0, 0, 30
    串行 GC 执行    :active, gc1, 30, 70
    空闲            :done, g1, 70, 100

Parallel GC:多线程 Serial,吞吐量优先

特点:GC 用多个线程并行工作,缩短 STW 时间,但仍然 STW。JDK 8 默认收集器,适用于后台计算任务,吞吐量优先。启用:-XX:+UseParallelGC

gantt
    title Parallel GC 时间轴(多线程并行,STW 更短)
    dateFormat  X
    axisFormat %s

    section 用户线程
    正常运行           :active, u1, 0, 30
    暂停(STW 更短)   :crit, stw1, 30, 55
    恢复运行           :active, u2, 55, 100

    section GC 线程 1
    等待               :done, g0, 0, 30
    并行 GC            :active, gc1, 30, 55
    空闲               :done, g10, 55, 100

    section GC 线程 2
    等待               :done, g20, 0, 30
    并行 GC            :active, gc2, 30, 55
    空闲               :done, g21, 55, 100

    section GC 线程 3
    等待               :done, g30, 0, 30
    并行 GC            :active, gc3, 30, 55
    空闲               :done, g31, 55, 100

CMS GC:并发收集,低停顿(已废弃)

目标:让 GC 和用户线程尽量并发执行,减少 STW 时间。JDK 14 中被废弃,建议迁移到 G1。

gantt
    title CMS GC 四阶段时间轴
    dateFormat  X
    axisFormat %s

    section 用户线程
    正常运行              :active, u1, 0, 10
    暂停(初始标记 STW)  :crit, stw1, 10, 18
    并发阶段同时运行      :active, u2, 18, 75
    暂停(重新标记 STW)  :crit, stw2, 75, 85
    并发清除同时运行      :active, u3, 85, 110

    section GC 线程
    等待                  :done, g0, 0, 10
    初始标记(STW 很短)  :crit, gc1, 10, 18
    并发标记(与用户并发):active, gc2, 18, 75
    重新标记(STW 较短)  :crit, gc3, 75, 85
    并发清除(与用户并发):active, gc4, 85, 110

CMS 存在的问题:① 并发清除时产生「浮动垃圾」;② 使用标记-清除,产生内存碎片;③ 老年代在并发阶段被填满时触发 Serial Old 兜底(极慢!)

G1 GC:分区设计,可预测停顿(JDK 9+ 默认)

G1 最大的创新:把堆划分为大量小的「Region」,每个 Region 约 1-32MB。

graph TD
    subgraph G1Heap[G1 堆布局(Region 视图)]
        subgraph Row1[第一行 Region]
            E1["E Eden Region"]
            E2["E Eden Region"]
            S1["S Survivor Region"]
            O1["O Old Region"]
            E3["E Eden Region"]
            O2["O Old Region"]
            H1["H Humongous Region (大对象)"]
            O3["O Old Region"]
        end
        subgraph Row2[第二行 Region]
            O4["O Old Region"]
            E4["E Eden Region"]
            H2["H Humongous Region (续)"]
            O5["O Old Region"]
            S2["S Survivor Region"]
            E5["E Eden Region"]
            O6["O Old Region"]
            Free["(空闲) 待分配"]
        end
    end

    Principle["G1 核心原则:优先回收「垃圾最多」的 Region 每个 Region 记录回收收益和代价 选择收益/代价最高的 Region 集合回收 可设定停顿目标:-XX:MaxGCPauseMillis=200"]

    style E1 fill:#fff3cd,stroke:#ffc107
    style E2 fill:#fff3cd,stroke:#ffc107
    style E3 fill:#fff3cd,stroke:#ffc107
    style E4 fill:#fff3cd,stroke:#ffc107
    style E5 fill:#fff3cd,stroke:#ffc107
    style S1 fill:#d1ecf1,stroke:#17a2b8
    style S2 fill:#d1ecf1,stroke:#17a2b8
    style O1 fill:#f8d7da,stroke:#dc3545
    style O2 fill:#f8d7da,stroke:#dc3545
    style O3 fill:#f8d7da,stroke:#dc3545
    style O4 fill:#f8d7da,stroke:#dc3545
    style O5 fill:#f8d7da,stroke:#dc3545
    style O6 fill:#f8d7da,stroke:#dc3545
    style H1 fill:#e2d9f3,stroke:#6f42c1
    style H2 fill:#e2d9f3,stroke:#6f42c1
    style Free fill:#e2e3e5,stroke:#6c757d
    style Principle fill:#d4edda,stroke:#28a745
flowchart TD
    Start(["开始 G1 Mixed GC"])

    Phase1["阶段 1:初始标记 🔴 STW(很短) 随 Minor GC 一起执行 标记 GC Roots 直接引用的对象"]

    Phase2["阶段 2:并发标记 🟢 与用户线程并发 从 GC Roots 遍历整个堆 记录每个 Region 的存活数据 (可能持续几百毫秒)"]

    Phase3["阶段 3:最终标记 🔴 STW(很短) 处理 SATB 缓冲区 修正并发阶段的引用变化"]

    Phase4["阶段 4:筛选回收 🔴 STW(可控时长) 按回收价值排序 Region 在停顿目标内选最有价值的 Region 复制存活对象到空 Region 原 Region 整体释放(无碎片)"]

    End(["GC 完成 内存已整理,无碎片"])

    Start --> Phase1 --> Phase2 --> Phase3 --> Phase4 --> End

    style Phase1 fill:#f8d7da,stroke:#dc3545
    style Phase2 fill:#d4edda,stroke:#28a745
    style Phase3 fill:#f8d7da,stroke:#dc3545
    style Phase4 fill:#fff3cd,stroke:#ffc107

ZGC:亚毫秒级停顿(JDK 15+ 生产可用)

ZGC 的目标:无论堆多大(TB 级),停顿时间始终 < 10ms(通常 1-2ms)。适用于金融交易、实时推荐、游戏服务器等延迟敏感场景。启用:-XX:+UseZGC(JDK 11+,生产推荐 JDK 15+)

gantt
    title ZGC 时间轴(几乎全程并发,STW 极短)
    dateFormat  X
    axisFormat %s

    section 用户线程
    持续运行(全程)     :active, u1, 0, 100

    section GC 线程
    并发标记             :active, gc1, 5, 35
    并发转移准备         :active, gc2, 35, 55
    并发转移(移动对象) :active, gc3, 55, 90
    并发重映射           :active, gc4, 90, 100

    section STW 停顿
    初始标记(~1ms)     :crit, stw1, 5, 7
    再标记(~1ms)       :crit, stw2, 33, 35

ZGC 关键技术:染色指针:64位指针高4位存储 GC 状态,无需额外标记位图;② 读屏障:每次读取引用时顺带完成重定位,将移动对象的引用更新分散到每次访问;③ 并发整理:对象可在用户线程运行时被移动。

五大收集器对比总结

收集器新生代算法老年代算法停顿特点适用场景
Serial复制标记整理STW,单线程客户端/单核/小堆
Parallel复制标记整理STW,多线程后台批处理,吞吐优先
CMS(配合 ParNew)标记清除短暂 STW + 并发已废弃,迁移到 G1
G1复制复制可设定目标停顿大堆(4GB+),通用服务
ZGC并发,全堆统一<10ms,与堆大小无关超大堆,延迟敏感服务

九、GC 调优实践

关键 JVM 参数

# 堆大小
-Xms4g -Xmx4g          # 初始和最大堆大小,建议设成相同值(避免动态扩容开销)
-Xmn1g                 # 新生代大小(一般为堆的 1/4 到 1/3)

# 选择收集器
-XX:+UseG1GC           # 使用 G1(JDK 9+ 默认)
-XX:+UseZGC            # 使用 ZGC(延迟敏感场景)

# G1 调优
-XX:MaxGCPauseMillis=200    # 期望停顿不超过 200ms
-XX:G1HeapRegionSize=16m    # Region 大小(1-32m,2 的幂次)

# GC 日志(必须开,出问题时救命)
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

常见问题诊断

问题 1:频繁 Full GC

诊断步骤:
  1. jstat -gcutil <pid> 1000   # 每秒看一次 GC 情况
  2. 观察 OU(Old 使用率)是否持续增长
  3. 如果 OU 线性增长 → 内存泄漏(对象无法被回收)
  4. 如果 OU 周期性满 → 老年代容量不足,考虑扩大 -Xmx 或 -Xmn

问题 2:GC 停顿过长

诊断步骤:
  1. 分析 GC 日志,找出停顿 > SLA 的 GC 事件
  2. 查看是 Minor GC 还是 Full GC
  3. Minor GC 慢 → 新生代太大,或存活对象太多(阈值设太高)
  4. Full GC 慢 → 老年代碎片(CMS),或堆太大(G1 Region 太多)

问题 3:OOM(OutOfMemoryError)

常见原因:
  java.lang.OutOfMemoryError: Java heap space
    → 堆内存不足,对象太多或内存泄漏

  java.lang.OutOfMemoryError: Metaspace
    → 动态生成类太多(如 AOP、反射、动态代理),
      增大 -XX:MaxMetaspaceSize

  java.lang.OutOfMemoryError: GC overhead limit exceeded
    → GC 时间占总时间比例超过 98%,几乎不做实际工作
      通常是内存严重不足的信号

十、关键点总结

  • 堆分新生代和老年代,基于分代假说:大多数对象短命,少数长寿;不同代用不同算法
  • Minor GC 用复制算法:Eden → Survivor,年龄到阈值晋升老年代;快速、频繁,影响小
  • Full GC 触发 Stop-The-World:整个堆暂停,代价极大,要尽量避免;老年代满、晋升失败是主要触发原因
  • 可达性分析从 GC Roots 出发判断存活,能正确处理循环引用(引用计数做不到)
  • 三种基础算法:标记-清除(有碎片)、标记-复制(无碎片但费内存)、标记-整理(无碎片,但移动对象开销大)
  • G1 是当前通用默认选择:Region 化设计,可设定停顿目标,无碎片,适合 4GB+ 的堆
  • ZGC 是延迟敏感场景的终极方案:染色指针 + 读屏障实现并发整理,停顿 <10ms 且与堆大小无关
  • GC 日志是调优的基础:生产环境必须开启 GC 日志,出问题时是最重要的诊断依据