编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
        • 3.1 开篇疑问
        • 3.2 为什么需要垃圾回收
        • 3.3 判断对象存活的算法
          • 3.3.1 引用计数法
          • 3.3.2 可达性分析法
          • 3.3.3 GC Roots完整枚举
          • 3.3.4 Java四种引用类型
          • 3.3.5 finalize方法与对象自救
        • 3.4 核心垃圾回收算法
          • 3.4.1 标记-清除算法
          • 3.4.2 复制算法
          • 3.4.3 标记-整理算法
          • 3.4.4 分代收集算法
          • 3.4.5 跨代引用与记忆集
          • 3.4.6 三色标记与并发标记
        • 3.5 经典垃圾收集器
          • 3.5.1 Serial与Serial Old
          • 3.5.2 ParNew与Parallel
          • 3.5.3 CMS收集器
          • 3.5.4 CMS的并发模式失败与退化
          • 3.5.5 G1收集器
          • 3.5.6 G1的Remembered Set与Card Table
          • 3.5.7 ZGC深度剖析
          • 3.5.8 Shenandoah收集器
          • 3.5.9 收集器组合搭配
        • 3.6 GC调优实战原理
          • 3.6.1 GC日志分析
          • 3.6.2 调优核心指标
          • 3.6.3 常见调优策略
          • 3.6.4 对象晋升机制详解
          • 3.6.5 GC调优实战案例
        • 3.7 JVM监控工具
        • 3.8 总结与核心要点
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

垃圾回收与GC调优

# 03.垃圾回收与GC调优

# 目录介绍

  • 3.1 开篇疑问
  • 3.2 为什么需要垃圾回收
  • 3.3 判断对象存活的算法
    • 3.3.1 引用计数法
    • 3.3.2 可达性分析法
    • 3.3.3 GC Roots完整枚举
    • 3.3.4 Java四种引用类型
    • 3.3.5 finalize方法与对象自救
  • 3.4 核心垃圾回收算法
    • 3.4.1 标记-清除算法
    • 3.4.2 复制算法
    • 3.4.3 标记-整理算法
    • 3.4.4 分代收集算法
    • 3.4.5 跨代引用与记忆集
    • 3.4.6 三色标记与并发标记
  • 3.5 经典垃圾收集器
    • 3.5.1 Serial与Serial Old
    • 3.5.2 ParNew与Parallel
    • 3.5.3 CMS收集器
    • 3.5.4 CMS的并发模式失败与退化
    • 3.5.5 G1收集器
    • 3.5.6 G1的Remembered Set与Card Table
    • 3.5.7 ZGC深度剖析
    • 3.5.8 Shenandoah收集器
    • 3.5.9 收集器组合搭配
  • 3.6 GC调优实战原理
    • 3.6.1 GC日志分析
    • 3.6.2 调优核心指标
    • 3.6.3 常见调优策略
    • 3.6.4 对象晋升机制详解
    • 3.6.5 GC调优实战案例
  • 3.7 JVM监控工具
  • 3.8 总结与核心要点

# 3.1 开篇疑问

疑惑:Java 号称不需要手动管理内存,那 GC 到底是怎么工作的?为什么有时候程序会出现长时间卡顿(Stop-The-World)?不同的垃圾收集器到底有什么区别,该怎么选?三色标记是什么?

答疑:垃圾回收是 JVM 最核心的子系统之一。从最早的 Serial 收集器到现代的 ZGC,GC 技术经历了近 30 年的演进。理解 GC 算法的原理和演变,不仅能帮你写出更高效的代码,还能在生产环境中快速定位和解决性能问题。

# 3.2 为什么需要垃圾回收

在 C/C++ 中,程序员需要手动管理内存(malloc/free、new/delete)。这带来两个典型问题:

  • 内存泄漏:忘记释放内存,程序长时间运行后内存耗尽
  • 野指针:释放后仍然访问,导致不可预期的行为

Java 通过自动垃圾回收解决了这两个问题,但代价是引入了 GC 停顿(Stop-The-World)。GC 技术的演进史,本质上就是一部不断缩短 STW 时间的历史。

时代 收集器 STW 时间 特点
JDK 1.3 Serial 秒级 单线程,全程 STW
JDK 1.4 Parallel 百毫秒级 多线程并行回收
JDK 1.5 CMS 十毫秒级 并发标记,减少 STW
JDK 1.7 G1 毫秒级可控 分区域,可设定目标停顿
JDK 11+ ZGC 亚毫秒级 染色指针,几乎无停顿

# 3.3 判断对象存活的算法

# 3.3.1 引用计数法

每个对象维护一个引用计数器,有引用加 1,引用失效减 1,计数为 0 的对象即可回收。

优点:实现简单,判定效率高。Python、Swift 等语言使用引用计数(配合其他机制处理循环引用)。

致命缺陷:无法处理循环引用。

// 循环引用示例
class Node {
    Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;  // b 的引用计数 = 2
b.next = a;  // a 的引用计数 = 2
a = null;    // a 指向对象的引用计数 = 1(b.next 还引用着)
b = null;    // b 指向对象的引用计数 = 1(a.next 还引用着)
// 两个对象都无法被外部访问,但引用计数不为0,永远无法回收
1
2
3
4
5
6
7
8
9
10
11
12

实验证明 JVM 不用引用计数:

public class ReferenceCountingGC {
    public Object instance = null;
    private byte[] bigSize = new byte[2 * 1024 * 1024]; // 2MB
    
    public static void main(String[] args) {
        ReferenceCountingGC a = new ReferenceCountingGC();
        ReferenceCountingGC b = new ReferenceCountingGC();
        a.instance = b;
        b.instance = a;
        
        a = null;
        b = null;
        
        System.gc();
        // GC 日志显示内存被回收了 → 证明 JVM 不用引用计数
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3.3.2 可达性分析法

JVM 采用可达性分析算法。从 GC Roots 出发,沿引用链遍历,不可达的对象即为可回收对象。

GC Roots
  ├── 栈帧中的局部变量
  ├── 静态变量
  ├── 常量
  └── JNI引用
       ↓ 沿引用链遍历
  Object A → Object B → Object C  (可达,存活)
  
  Object D → Object E  (从GC Roots不可达,可回收)
       ↑________↓       (即使互相引用也会被回收)
1
2
3
4
5
6
7
8
9
10

# 3.3.3 GC Roots完整枚举

能作为 GC Roots 的对象包括:

GC Root 类型 说明
虚拟机栈中的局部变量 当前所有活跃线程的栈帧中引用的对象
方法区中的静态变量 类的 static 字段引用的对象
方法区中的常量 字符串常量池中的引用
JNI 引用 Native 方法持有的引用
JVM 内部引用 基本类型的 Class 对象、系统类加载器等
synchronized 持有的对象 被 monitor lock 持有的对象
JMXBean、JVMTI 中注册的回调
跨代引用(部分GC时) 老年代中引用新生代对象

枚举根节点的 OopMap:

问:GC 时如何快速找到所有 GC Roots?逐个扫描栈和寄存器太慢了。

答:JVM 使用 OopMap(Ordinary Object Pointer Map)数据结构。
在类加载完成时,JVM 就把对象内什么偏移量上是什么类型的引用计算出来。
在 JIT 编译时,也会在特定的位置记录栈和寄存器中哪些位置是引用。

这些特定位置就是"安全点"(Safepoint)。
GC 不是在任意位置暂停线程,而是等线程到达安全点后才暂停。
1
2
3
4
5
6
7
8

安全点(Safepoint)与安全区域(Safe Region):

// 安全点的典型位置:
// 1. 方法调用处
// 2. 循环跳转处
// 3. 异常抛出处
// 这些位置可以确保 OopMap 是最新的

// 如何让线程跑到安全点?两种方式:
// 1. 抢先式中断(Preemptive Suspension):中断所有线程,没到安全点的恢复执行
//    → 几乎没有实现使用这种方式
// 2. 主动式中断(Voluntary Suspension):设置标志位,线程轮询到标志时自行中断
//    → 主流实现方式

// 安全区域:线程处于 sleep 或 blocked 状态时,无法走到安全点
// 安全区域 = 引用关系不会变化的代码区域
// 线程进入安全区域时标记自己,GC 直接忽略这些线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.3.4 Java四种引用类型

JDK 1.2 引入了四种引用类型,让开发者可以更细粒度地控制对象的生命周期:

引用类型 回收时机 用途 类
强引用 永不回收(只要可达) 普通赋值 Object o = new Object() -
软引用 内存不足时回收 缓存 SoftReference<T>
弱引用 下次 GC 时回收 WeakHashMap、ThreadLocal WeakReference<T>
虚引用 随时回收 跟踪对象回收,堆外内存清理 PhantomReference<T>
// 软引用做缓存
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024 * 10]);

byte[] data = cache.get();
if (data != null) {
    // 缓存命中,直接使用
} else {
    // 内存不足被回收了,重新加载
    data = loadFromDisk();
    cache = new SoftReference<>(data);
}

// 配合引用队列使用
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
SoftReference<byte[]> ref = new SoftReference<>(new byte[1024], queue);
// 当 ref 被回收后,ref 会被加入 queue
// 可以通过 queue.poll() 获取被回收的引用,做清理工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

虚引用的典型应用——堆外内存管理:

// DirectByteBuffer 使用虚引用跟踪对象回收
// 当 DirectByteBuffer 对象被 GC 回收时
// 其关联的虚引用(Cleaner)被放入 ReferenceQueue
// Cleaner 线程从队列中取出虚引用,释放堆外内存

ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 内部创建了 Cleaner(虚引用的子类)
// buffer 被 GC 回收时,Cleaner.clean() 自动调用 Unsafe.freeMemory()
1
2
3
4
5
6
7
8

# 3.3.5 finalize方法与对象自救

对象被可达性分析判定为不可达后,并不会立即被回收,还有一次"自救"的机会:

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize 被执行");
        // 自救:把自己重新赋给一个 GC Root 可达的引用
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws Exception {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        // 第一次自救:成功
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        System.out.println(SAVE_HOOK != null ? "存活" : "死亡"); // 存活
        
        // 第二次自救:失败(finalize 只会被调用一次)
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        System.out.println(SAVE_HOOK != null ? "存活" : "死亡"); // 死亡
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

注意:finalize() 已被 JDK 9 标记为 @Deprecated,不推荐使用。原因:

  1. 执行时机不确定
  2. 严重影响 GC 性能(Finalizer 线程优先级低,可能导致对象长时间不被回收)
  3. 可能导致对象复活
  4. 推荐使用 try-with-resources 或 Cleaner(JDK 9+)替代

# 3.4 核心垃圾回收算法

# 3.4.1 标记-清除算法

Mark-Sweep 是最基础的 GC 算法,分两步:

  1. 标记:从 GC Roots 遍历,标记所有可达对象
  2. 清除:遍历堆,回收未被标记的对象
标记前:   [A][B][ ][C][ ][D][E][ ][F]
标记:     [A][B][ ][C][ ][ ][E][ ][ ]   (D, F 不可达)
清除后:   [A][B][空][C][空][空][E][空][空]
1
2
3

缺点:

  • 效率不稳定:堆中对象越多,标记和清除耗时越长
  • 内存碎片:清除后产生大量不连续的空闲内存块,可能导致大对象无法分配

# 3.4.2 复制算法

Copying 算法将内存分为两块,每次只使用一块。GC 时将存活对象复制到另一块,然后清空当前块。

复制前:
  From: [A][B][空][C][空][D]    To: [空][空][空][空][空][空]
  
复制后(A,B,C存活,D不可达):
  From: [空][空][空][空][空][空]   To: [A][B][C][空][空][空]
1
2
3
4
5

优点:没有内存碎片,分配效率高(指针碰撞) 缺点:可用内存缩小为一半

优化:新生代采用 Eden + 2×Survivor 的 8:1:1 布局,只浪费 10% 空间。因为新生代对象 98% 以上都活不过一次 GC,复制的对象很少。

新生代内存布局:
┌──────────────────┬────┬────┐
│     Eden (80%)    │ S0 │ S1 │
│                   │10% │10% │
└──────────────────┴────┴────┘

Minor GC 过程:
1. 新对象分配在 Eden
2. Eden 满 → 触发 Minor GC
3. Eden + S0 中存活对象 → 复制到 S1
4. 清空 Eden + S0
5. S0 和 S1 角色互换(From/To)
6. 重复...
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.4.3 标记-整理算法

Mark-Compact 在标记后不直接清除,而是将所有存活对象向内存一端移动,然后清理边界以外的内存。

标记后:   [A][空][B][空][空][C][空][D]
整理后:   [A][B][C][D][空][空][空][空]
1
2

优点:没有内存碎片 缺点:移动对象需要更新所有引用,且必须 STW

关键取舍:标记-清除不需要移动对象但有碎片;标记-整理没有碎片但需要移动。CMS 选择了前者(追求低延迟),Parallel Old 选择了后者(追求高吞吐)。

# 3.4.4 分代收集算法

分代收集不是一种新算法,而是将上述算法按对象特征组合使用:

堆内存
├── 新生代(对象存活率低 ~2%)
│   └── 复制算法(Eden → Survivor,只复制少量存活对象)
└── 老年代(对象存活率高 ~70%+)
    └── 标记-清除 或 标记-整理
1
2
3
4
5

为什么这样设计?

弱分代假说:绝大多数对象朝生夕死。强分代假说:熬过多次 GC 的对象越难消亡。基于这两个假说,对不同"年龄"的对象采用不同策略,是最优解。

# 3.4.5 跨代引用与记忆集

问题:Minor GC 只回收新生代,但如果老年代有对象引用新生代怎么办?需要扫描整个老年代吗?

解决方案:记忆集(Remembered Set)+ 卡表(Card Table)

老年代被划分为多个"卡页"(Card Page,通常 512 字节)
每个卡页对应卡表中的一个字节

卡表: [0][0][1][0][1][0][0][0]
              ↑       ↑
         这个卡页有    这个卡页有
         跨代引用      跨代引用

Minor GC 时,只需要扫描"脏卡页",不需要遍历整个老年代
1
2
3
4
5
6
7
8
9
// 卡表的本质:一个字节数组
// CARD_TABLE[address >> 9] = 1  (脏卡)
// 512 字节 = 2^9,右移9位就是卡页索引

// 写屏障(Write Barrier)维护卡表
// 每次引用赋值时,JVM 自动更新卡表
// 伪代码:
void oop_field_store(Object* field, Object* new_value) {
    *field = new_value;
    // 写后屏障:标记卡表
    CARD_TABLE[field >> 9] = DIRTY;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 3.4.6 三色标记与并发标记

CMS 和 G1 在并发标记阶段使用三色标记算法:

白色:尚未访问的对象(GC 开始时所有对象都是白色)
灰色:已被访问,但其引用的对象还未全部访问
黑色:已被访问,且其引用的对象也都已访问

标记过程:
1. 初始:所有对象白色
2. 将 GC Roots 直接引用的对象标为灰色
3. 从灰色对象出发,遍历其引用的对象,标为灰色
4. 自身标为黑色
5. 重复 3-4 直到没有灰色对象
6. 剩余的白色对象即为垃圾
1
2
3
4
5
6
7
8
9
10
11

并发标记的问题——漏标:

并发标记期间,用户线程可能修改引用关系,导致两种问题:

1. 浮动垃圾(多标):本该回收的对象被标记为存活
   → 不影响正确性,下次GC回收即可

2. 漏标(对象消失):存活对象被当成垃圾回收
   → 严重问题!必须解决

漏标发生条件(同时满足):
  a. 一个黑色对象新增了指向白色对象的引用
  b. 所有灰色对象到该白色对象的引用都被删除

解决方案:
  1. 增量更新(Incremental Update):记录新增的引用
     → CMS 使用
  2. 原始快照(SATB, Snapshot At The Beginning):记录被删除的引用
     → G1 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

CMS 的增量更新:

黑色对象 A 新增引用 → 白色对象 C
增量更新:记录 A → C 这条新引用
重新标记阶段:从 A 出发重新扫描
1
2
3

G1 的 SATB:

灰色对象 B 删除引用 → 白色对象 C
SATB:在删除前记录 B → C 这条旧引用
最终标记阶段:以这条旧引用为根,扫描 C
相当于"快照"了标记开始时的引用关系
1
2
3
4

# 3.5 经典垃圾收集器

# 3.5.1 Serial与Serial Old

Serial (新生代,复制算法)
  线程: ──────│STW: 单线程GC│──────
              │←  GC 停顿  →│

Serial Old (老年代,标记-整理)
  同理,单线程
1
2
3
4
5
6

特点:简单高效,适合客户端模式和内存较小的场景。单核 CPU 下没有线程切换开销,效率反而最高。

使用场景:

  • 客户端应用(桌面程序)
  • 嵌入式设备
  • 堆内存 < 100MB 的应用
-XX:+UseSerialGC  # 新生代 Serial + 老年代 Serial Old
1

# 3.5.2 ParNew与Parallel

ParNew 是 Serial 的多线程版本,Parallel Scavenge 关注吞吐量。

Parallel (新生代)
  线程1: ────│STW: GC线程1│──────
  线程2: ────│STW: GC线程2│──────
  线程3: ────│STW: GC线程3│──────
              │← GC停顿 →│
1
2
3
4
5

ParNew vs Parallel Scavenge 的区别:

特性 ParNew Parallel Scavenge
关注点 低延迟(配合CMS) 高吞吐量
框架 基于 Serial 改造 独立实现
配合的老年代 CMS Parallel Old
自适应调节 无 -XX:+UseAdaptiveSizePolicy

Parallel Scavenge 的独特设计:提供两个参数精确控制吞吐量:

  • -XX:MaxGCPauseMillis:最大 GC 停顿时间
  • -XX:GCTimeRatio:GC 时间占总时间的比率

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)

# Parallel 收集器(JDK 8 默认)
-XX:+UseParallelGC      # 新生代 Parallel Scavenge
-XX:+UseParallelOldGC   # 老年代 Parallel Old
-XX:ParallelGCThreads=4 # GC 线程数
-XX:MaxGCPauseMillis=100 # 最大停顿目标 100ms
-XX:GCTimeRatio=99       # 吞吐量目标 99%
1
2
3
4
5
6

# 3.5.3 CMS收集器

CMS(Concurrent Mark Sweep)是第一款真正意义上的并发收集器,以最短停顿时间为目标。

四个阶段:

阶段1: 初始标记 (STW)    ← 极短,只标记GC Roots直接引用的对象
阶段2: 并发标记           ← 与用户线程并发执行,遍历引用链
阶段3: 重新标记 (STW)    ← 修正并发标记期间变动的标记(增量更新)
阶段4: 并发清除           ← 与用户线程并发执行,清除死亡对象

时间线:
用户线程: ──│STW│────并发────│STW│────并发────→
GC线程:    │标记│────标记────│重标│────清除────→
1
2
3
4
5
6
7
8

CMS 的三大缺陷:

  1. CPU 敏感:并发阶段占用 CPU 资源,默认 GC 线程数 = (CPU核心数 + 3) / 4
  2. 浮动垃圾:并发清除阶段用户线程还在产生新垃圾,只能等下次 GC 清理
  3. 内存碎片:基于标记-清除算法,会产生碎片。碎片过多时会触发 Serial Old 做 Full GC

# 3.5.4 CMS的并发模式失败与退化

# CMS 核心参数
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75  # 老年代使用 75% 时触发 CMS GC
-XX:+UseCMSCompactAtFullCollection     # Full GC 时整理碎片
-XX:CMSFullGCsBeforeCompaction=5       # 多少次 Full GC 后做一次碎片整理
1
2
3
4
5

Concurrent Mode Failure:

并发清除阶段,用户线程还在分配对象
如果老年代空间不足以容纳新晋升的对象
→ Concurrent Mode Failure
→ CMS 退化为 Serial Old 进行 Full GC(灾难性的长停顿!)

解决方案:
1. 降低 CMSInitiatingOccupancyFraction,提前触发 CMS
2. 增大老年代空间
3. 优化对象分配,减少晋升压力
1
2
3
4
5
6
7
8
9

Promotion Failed:

Minor GC 时,存活对象需要晋升到老年代
但老年代因为碎片化,虽然总空间足够但没有连续空间
→ Promotion Failed
→ 触发 Full GC

解决方案:
1. 开启碎片整理:-XX:+UseCMSCompactAtFullCollection
2. 增大老年代空间
3. 考虑迁移到 G1
1
2
3
4
5
6
7
8
9

# 3.5.5 G1收集器

G1(Garbage-First)是 JDK 9 的默认收集器,开创了基于 Region 的内存布局:

┌────┬────┬────┬────┬────┬────┬────┬────┐
│ E  │ S  │ O  │ H  │ E  │ O  │ E  │ S  │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ O  │ E  │    │ O  │ S  │ E  │ O  │    │
├────┼────┼────┼────┼────┼────┼────┼────┤
│ E  │ O  │ O  │ E  │ O  │    │ E  │ O  │
└────┴────┴────┴────┴────┴────┴────┴────┘
E=Eden  S=Survivor  O=Old  H=Humongous(大对象)
空白=Free Region
1
2
3
4
5
6
7
8
9

核心设计思想:

  • 将堆划分为大小相等的 Region(1~32MB),每个 Region 可以扮演不同角色
  • 跟踪每个 Region 的垃圾回收价值(回收空间 / 回收时间),优先回收价值最大的 Region
  • 用户可以设定期望停顿时间 -XX:MaxGCPauseMillis=200,G1 会尽量在这个时间内完成回收

G1 的三种 GC 模式:

1. Young GC:只回收所有 Eden 和 Survivor Region
   → 存活对象复制到新的 Survivor / Old Region
   → 类似新生代 Minor GC

2. Mixed GC:回收所有 Young Region + 部分 Old Region
   → "部分" = 回收价值最高的 Old Region
   → 这就是 "Garbage-First" 名字的由来

3. Full GC:回收所有 Region(Serial Old 串行执行)
   → 通常意味着 G1 调优不佳
1
2
3
4
5
6
7
8
9
10

G1 的 Mixed GC 过程:

  1. 初始标记(STW):标记 GC Roots 直接关联的对象(借助 Young GC 完成)
  2. 并发标记:从 GC Roots 开始遍历整个堆(SATB 算法)
  3. 最终标记(STW):处理并发标记阶段 SATB 记录的变化
  4. 筛选回收(STW):对 Region 排序,选择回收价值最高的若干 Region 回收

Humongous 对象:

// 大小超过 Region 一半的对象被称为 Humongous 对象
// 直接分配在 Humongous Region 中(可能跨多个连续 Region)
// G1 会在 Young GC 和 Mixed GC 时同时回收 Humongous Region

// 如果 Region 大小为 2MB,则 > 1MB 的对象就是 Humongous
// 可以通过 -XX:G1HeapRegionSize 调整 Region 大小
1
2
3
4
5
6

# 3.5.6 G1的Remembered Set与Card Table

G1 的每个 Region 都有自己的 Remembered Set(RSet),记录其他 Region 到本 Region 的引用:

Region A 中有对象引用了 Region B 中的对象
→ Region B 的 RSet 中记录: "Region A 的某个位置引用了我"

Minor GC 回收 Region B 时:
→ 不需要扫描所有其他 Region
→ 只需查看 Region B 的 RSet,就知道哪些外部引用指向 B
1
2
3
4
5
6

RSet 的内存开销:RSet 通常占堆内存的 5%~20%。这是 G1 的一个缺点——空间换时间。

# G1 核心参数
-XX:+UseG1GC                      # 使用 G1(JDK 9+ 默认)
-XX:MaxGCPauseMillis=200          # 目标停顿时间 200ms
-XX:G1HeapRegionSize=4m           # Region 大小(1~32MB,2的幂)
-XX:InitiatingHeapOccupancyPercent=45  # 堆使用 45% 时触发并发标记
-XX:G1MixedGCCountTarget=8        # Mixed GC 次数目标
1
2
3
4
5
6

# 3.5.7 ZGC深度剖析

ZGC(JDK 11+)代表了 GC 技术的最前沿,目标是将停顿时间控制在 10ms 以内,且不随堆大小增长。

ZGC 核心技术——染色指针(Colored Pointers):

64位指针布局 (ZGC):
┌──────────┬───┬───┬───┬───┬─────────────────────┐
│ 未使用    │ F │ R │ M1│ M0│    对象地址 (42位)     │
│ (16bit)  │   │   │   │   │    支持 4TB 堆        │
└──────────┴───┴───┴───┴───┴─────────────────────┘
F=Finalizable  R=Remapped  M0/M1=Marked0/Marked1
1
2
3
4
5
6

ZGC 将 GC 信息存储在指针中(而非对象头),配合读屏障(Load Barrier),实现了几乎完全并发的垃圾回收。

ZGC 的工作流程:

1. 初始标记 (STW):标记 GC Roots(极短,<1ms)
2. 并发标记:遍历对象图,通过染色指针标记存活对象
3. 再标记 (STW):处理少量变化(极短)
4. 并发转移准备:选择要回收的 Region
5. 初始转移 (STW):转移 GC Roots 直接引用的对象(极短)
6. 并发转移:将存活对象复制到新 Region(核心!并发执行)

并发转移的关键:读屏障(Load Barrier)
→ 用户线程读取对象引用时,如果发现染色指针中标记了"已转移"
→ 自动转发到新地址(类似 HTTP 301 重定向)
→ 这就是为什么不需要 STW 来更新所有引用
1
2
3
4
5
6
7
8
9
10
11

ZGC vs G1:

特性 G1 ZGC
最大停顿 ~200ms(可控但较大) <10ms
堆大小支持 ~64GB ~16TB
内存占用 RSet 占 5-20% 染色指针无额外占用
碎片处理 Region 间复制 Region 间复制
并发程度 标记并发,回收 STW 标记+转移都并发
JDK 版本 JDK 7+ JDK 11+(JDK 15 正式)
适用场景 通用 超低延迟要求
# ZGC 参数
-XX:+UseZGC                       # 使用 ZGC
-XX:ZAllocationSpikeTolerance=5   # 分配尖峰容忍度
-XX:ZCollectionInterval=120       # GC 间隔(秒)
-Xmx16g                          # 堆大小
1
2
3
4
5

# 3.5.8 Shenandoah收集器

Shenandoah(JDK 12+,Red Hat 贡献)的目标与 ZGC 类似,但实现方式不同:

特性 ZGC Shenandoah
核心技术 染色指针 Brooks Pointer(转发指针)
内存屏障 读屏障 读+写屏障
堆大小 最大 16TB 理论无限制
开发者 Oracle Red Hat
可用 JDK Oracle/OpenJDK 仅 OpenJDK

# 3.5.9 收集器组合搭配

新生代收集器          老年代收集器          说明
─────────────────────────────────────────────────
Serial           →   Serial Old         单线程,客户端模式
ParNew           →   CMS               低延迟(JDK 8 常用)
ParNew           →   Serial Old         CMS 退化时的后备
Parallel Scavenge →  Parallel Old       高吞吐(JDK 8 默认)
G1               →   G1                 分区域收集(JDK 9+ 默认)
ZGC              →   ZGC                超低延迟(JDK 15+)
Shenandoah       →   Shenandoah         超低延迟(OpenJDK)
1
2
3
4
5
6
7
8
9

收集器选择决策树:

                  需要最低延迟?
                  /          \
               是              否
              /                  \
         堆 > 4GB?            需要高吞吐?
         /       \            /          \
        是        否         是            否
       ZGC    CMS/G1    Parallel    Serial/G1
1
2
3
4
5
6
7
8

# 3.6 GC调优实战原理

# 3.6.1 GC日志分析

开启 GC 日志是调优的第一步:

# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/gc.log
-XX:+PrintGCApplicationStoppedTime  # 打印 STW 时间
-XX:+PrintHeapAtGC                   # GC 前后打印堆信息

# JDK 9+(统一日志框架)
-Xlog:gc*:file=/path/gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug:file=/path/gc.log  # 详细堆信息
1
2
3
4
5
6
7
8

关键日志解读:

[GC (Allocation Failure) 
  [PSYoungGen: 153600K->2048K(153600K)]   // 新生代: 回收前->回收后(总容量)
  153600K->13056K(502784K),               // 堆: 回收前->回收后(总容量)
  0.0123456 secs]                         // 耗时

// 153600K - 2048K = 151552K  (新生代回收量)
// 153600K - 13056K = 140544K (堆回收量)  
// 差值 151552K - 140544K = 11008K (晋升到老年代的对象)
1
2
3
4
5
6
7
8

G1 日志解读:

[GC pause (G1 Evacuation Pause) (young), 0.0134567 secs]
   [Parallel Time: 12.3 ms, GC Workers: 4]
      [GC Worker Start: Min: 1234.5, Avg: 1234.5, Max: 1234.6]
      [Ext Root Scanning: Min: 0.1, Avg: 0.2, Max: 0.3]
      [Update RS: Min: 1.2, Avg: 1.5, Max: 1.8]
      [Scan RS: Min: 0.5, Avg: 0.7, Max: 0.9]
      [Code Root Scanning: 0.0]
      [Object Copy: Min: 8.1, Avg: 8.5, Max: 9.0]
      [Termination: Min: 0.0, Avg: 0.1, Max: 0.2]
   [Code Root Fixup: 0.1 ms]
   [Clear CT: 0.2 ms]
   [Other: 1.0 ms]
   [Eden: 24.0M(24.0M)->0.0B(24.0M) 
    Survivors: 4096.0K->4096.0K 
    Heap: 112.0M(256.0M)->95.0M(256.0M)]
 [Times: user=0.05 sys=0.00, real=0.01 secs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3.6.2 调优核心指标

指标 含义 目标
吞吐量 用户代码运行时间占比 > 95%
停顿时间 单次 GC 暂停时间 < 200ms(Web 应用)
GC 频率 单位时间 GC 次数 Minor GC: 秒级;Full GC: 小时级以上
内存占用 堆使用情况 不超过 70%

疑惑:吞吐量和停顿时间能不能同时做到最优?

论证:不能。这是一对矛盾的目标。降低停顿时间意味着更频繁的小规模 GC,但总的 GC 时间可能增加(吞吐量下降)。追求高吞吐量意味着减少 GC 次数,但单次 GC 时间会更长(停顿增加)。必须根据业务场景做取舍:

  • 批处理任务:追求吞吐量 → Parallel
  • Web 应用:追求低延迟 → G1 / ZGC
  • 实时系统:追求极低延迟 → ZGC

# 3.6.3 常见调优策略

1. 合理设置堆大小:

# 原则:初始堆 = 最大堆,避免堆大小动态调整带来的开销
-Xms4g -Xmx4g
# 新生代大小
-Xmn2g
# 或通过比例设置
-XX:NewRatio=2  # 老年代:新生代 = 2:1
1
2
3
4
5
6

2. 选择合适的收集器:

# JDK 8 默认: Parallel
# JDK 9+ 默认: G1
# 低延迟场景
-XX:+UseZGC              # JDK 11+
# 吞吐量优先
-XX:+UseParallelGC       # JDK 8
1
2
3
4
5
6

3. 新生代调优:

# 增大新生代,减少 Minor GC 频率
-Xmn2g
# 调整 Eden/Survivor 比例
-XX:SurvivorRatio=8  # Eden:S0:S1 = 8:1:1
# 设置对象晋升阈值
-XX:MaxTenuringThreshold=15  # 默认15,对象经过15次Minor GC后晋升老年代
1
2
3
4
5
6

4. 避免 Full GC:

  • 大对象(长字符串、大数组)直接进老年代,合理设置 -XX:PretenureSizeThreshold
  • 避免在短时间内创建大量临时对象(如循环中拼接字符串用 StringBuilder)
  • 检查是否有内存泄漏(对象持续累积不释放)

# 3.6.4 对象晋升机制详解

对象从新生代晋升到老年代的条件:

1. 年龄达到阈值
   → 每经过一次 Minor GC 且存活,年龄 +1
   → 年龄 >= MaxTenuringThreshold(默认15)时晋升
   → 年龄存储在对象头的 Mark Word 中(4bit,最大15)

2. 动态年龄判定
   → Survivor 中,某个年龄的对象大小总和 > Survivor 的一半
   → 大于等于该年龄的所有对象直接晋升
   → 不需要等到 MaxTenuringThreshold

3. 大对象直接进老年代
   → -XX:PretenureSizeThreshold(仅 Serial + ParNew 有效)
   → G1 中大于 Region 一半的对象直接进 Humongous

4. Minor GC 后 Survivor 放不下
   → 通过空间分配担保进入老年代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

空间分配担保:

// Minor GC 前的检查流程(JDK 6u24+):
// 1. 老年代最大可用连续空间 > 新生代所有对象总空间?
//    → 是:安全,直接 Minor GC
//    → 否:检查 HandlePromotionFailure(JDK 6u24+ 始终为 true)
// 2. 老年代最大可用连续空间 > 历次晋升平均大小?
//    → 是:冒险进行 Minor GC
//    → 否:改为 Full GC
1
2
3
4
5
6
7

# 3.6.5 GC调优实战案例

案例1:频繁 Full GC

现象:每隔几分钟发生一次 Full GC,停顿 2-3 秒
排查:
  1. jstat -gcutil PID 1000  → 发现老年代快速增长
  2. jmap -histo PID         → 发现大量 byte[] 和 String
  3. MAT 分析 dump 文件      → 发现缓存未设上限

原因:本地缓存(HashMap)持续增长,对象不断晋升到老年代
解决:
  1. 改用 LRU 缓存(如 Caffeine),设置最大容量
  2. 考虑使用弱引用缓存(WeakHashMap)
  3. 增大堆内存作为临时方案
1
2
3
4
5
6
7
8
9
10
11

案例2:Minor GC 耗时长

现象:Minor GC 耗时 100ms+,正常应 < 50ms
排查:
  1. -XX:+PrintGCDetails 发现大量对象存活
  2. 存活对象 > Survivor 容量 → 直接晋升老年代

原因:Survivor 太小,大量对象被迫晋升
解决:
  1. 增大 Survivor:-XX:SurvivorRatio=4(Eden:S0:S1=4:1:1)
  2. 增大新生代总大小
  3. 增大晋升阈值:-XX:MaxTenuringThreshold=15
1
2
3
4
5
6
7
8
9
10

案例3:CMS 退化为 Serial Old

现象:偶尔出现 10 秒级长停顿
日志:[GC (CMS Initial Mark) ... Concurrent Mode Failure]

原因:CMS 并发清除时老年代空间不足
解决:
  1. 降低触发阈值:-XX:CMSInitiatingOccupancyFraction=60
  2. 增大老年代空间
  3. 迁移到 G1
1
2
3
4
5
6
7
8

# 3.7 JVM监控工具

工具 用途 命令示例
jps 查看 Java 进程 jps -l
jstat GC 统计 jstat -gcutil PID 1000
jmap 堆内存分析 jmap -histo PID
jstack 线程栈分析 jstack PID
jinfo JVM 参数 jinfo -flags PID
jcmd 综合诊断 jcmd PID GC.heap_info
# 常用命令
jstat -gcutil PID 1000 10  # 每秒输出 GC 统计,共10次

# 输出说明:
# S0    S1    E     O     M     CCS   YGC  YGCT  FGC  FGCT   GCT
# 0.00  82.5  45.3  56.2  95.1  92.3  125  1.23   3   0.35  1.58
# S0/S1: Survivor使用率  E: Eden使用率  O: Old使用率
# YGC: Young GC次数  YGCT: Young GC总耗时  FGC: Full GC次数

# 导出堆转储
jmap -dump:live,format=b,file=heap.hprof PID
# 或
jcmd PID GC.heap_dump /path/heap.hprof
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.8 总结与核心要点

GC 算法的演进逻辑:

单线程全停顿 (Serial)
  → 多线程全停顿 (Parallel)        // 用多核加速GC
    → 并发标记+清除 (CMS)          // 减少STW阶段
      → 分区域+可控停顿 (G1)       // 精细化管理
        → 染色指针+全并发 (ZGC)    // 极致低延迟
1
2
3
4
5

核心设计思想:

  1. 分治:分代/分区是对"不同对象不同策略"的分治思想
  2. 空间换时间:复制算法牺牲空间换取零碎片;卡表(Card Table)牺牲空间换取跨代引用的快速定位;G1 的 RSet 牺牲空间换取独立回收
  3. 并发化:从全程 STW 到部分并发再到几乎完全并发,这是降低停顿的核心方向
  4. 可控性:从不可预测到可设定目标停顿时间,让 GC 行为可控可观测
  5. 读/写屏障:CMS 用写屏障维护增量更新,G1 用写屏障维护 SATB 和 RSet,ZGC 用读屏障实现并发转移

核心要点速查:

问题 答案
JVM 用什么判断对象存活 可达性分析(从 GC Roots 出发)
新生代用什么算法 复制算法(Eden + 2×Survivor)
老年代用什么算法 标记-清除(CMS)或标记-整理(Parallel Old)
CMS 的核心问题 内存碎片 + Concurrent Mode Failure
G1 的核心优势 可控停顿 + Region 化管理
ZGC 的核心技术 染色指针 + 读屏障
三色标记的漏标问题 CMS 用增量更新,G1 用 SATB
Full GC 怎么避免 合理堆大小 + 控制对象晋升 + 避免内存泄漏
对象晋升条件 年龄达标 / 动态年龄判定 / 大对象 / Survivor 放不下

理解 GC 原理,是 Java 性能调优和生产环境问题排查的核心能力。

上次更新: 2026/06/10, 11:13:41
类加载与双亲委派
异常体系与JVM机制

← 类加载与双亲委派 异常体系与JVM机制→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式