3深入理解 Java 虚拟机(第3版)

垃圾收集器与内存分配策略

自动内存管理的核心

引用计数可达性分析垃圾收集算法SerialParallelCMSG1

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章:垃圾收集器与内存分配策略

一、导读

3.1 本章学习目标

本章讲解垃圾收集的原理和实现:如何判断对象已死、垃圾收集算法、以及各种垃圾收集器的工作原理。

本章的核心学习目标包括:

  • 掌握垃圾收集的判断算法:引用计数法、可达性分析
  • 理解垃圾收集算法:标记-清除、标记-复制、标记-整理、分代收集
  • 掌握主流垃圾收集器:Serial、Parallel、CMS、G1
  • 理解对象分配策略:TLAB、优先在 Eden 区分配

3.2 为什么这一章如此重要

垃圾收集是 JVM 的核心功能。理解垃圾收集的原理,对于:

  • 优化 GC 性能:选择合适的收集器
  • 减少 GC 停顿:调优 GC 参数
  • 避免内存泄漏:理解对象的生命周期
  • 提升应用性能:减少 GC 对业务的影响

二、核心概念详解

2.1 如何判断对象已死

2.1.1 引用计数法

每个对象维护一个引用计数器,引用加 1,释放减 1。计数器为 0 时,对象已死。

优点:简单高效

缺点:无法解决循环引用

java
// 循环引用示例
public class ReferenceCounting {
    public Object partner;
    
    public static void main(String[] args) {
        ReferenceCounting a = new ReferenceCounting();
        ReferenceCounting b = new ReferenceCounting();
        a.partner = b;
        b.partner = a;
        
        a = null;
        b = null;
        // a 和 b 的引用计数都不为 0,但已经无法访问
    }
}

2.1.2 可达性分析

从 GC Roots 开始搜索,搜索到的对象是存活的,未搜索到的对象是已死的。

GC Roots 包括:

  • 虚拟机栈中引用的对象(局部变量)
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象
java
// 可达性分析示例
public class ReachabilityAnalysis {
    public static void main(String[] args) {
        Object obj = new Object();  // obj 是 GC Root
        // obj 可达,不会被回收
        
        obj = null;  // obj 不可达,会被回收
    }
}

2.2 垃圾收集算法

2.2.1 标记-清除算法(Mark-Sweep)

过程:

标记所有存活对象

清除所有未标记对象

优点:简单

缺点:内存碎片

标记前:
┌─────┬─────┬─────┬─────┬─────┐
│  A  │  B  │  C  │  D  │  E  │
│存活 │死亡 │存活 │死亡 │存活 │
└─────┴─────┴─────┴─────┴─────┘

清除后:
┌─────┬     ┬─────┬     ┬─────┐
│  A  │     │  C  │     │  E  │
└─────┴─────┴─────┴─────┴─────┘
   ↑ 内存碎片

2.2.2 标记-复制算法(Mark-Copy)

过程:

将内存分为两半

标记存活对象

将存活对象复制到另一半

清除原来的内存

优点:无内存碎片

缺点:浪费一半内存

复制前:
┌─────────────┬─────────────┐
│   From      │    To       │
│ A C E       │             │
└─────────────┴─────────────┘

复制后:
┌─────────────┬─────────────┐
│   From      │    To       │
│             │ A C E       │
└─────────────┴─────────────┘

2.2.3 标记-整理算法(Mark-Compact)

过程:

标记所有存活对象

将存活对象向一端移动

清除边界外的内存

优点:无内存碎片

缺点:移动对象开销大

整理前:
┌─────┬     ┬─────┬     ┬─────┐
│  A  │     │  C  │     │  E  │
└─────┴─────┴─────┴─────┴─────┘

整理后:
┌─────┬─────┬─────┬     │     │
│  A  │  C  │  E  │     │     │
└─────┴─────┴─────┴─────┴─────┘

2.2.4 分代收集算法

根据对象的存活周期,将堆分为新生代和老年代。

  • 新生代:使用标记-复制算法(存活率高)
  • 老年代:使用标记-清除或标记-整理算法(存活率低)

2.3 主流垃圾收集器

2.3.1 Serial 收集器

特点:单线程收集器

适用场景:客户端模式

┌─────────────────────────────────┐
│         应用线程暂停             │
├─────────────────────────────────┤
│  Serial GC 线程(单线程)        │
└─────────────────────────────────┘

运行参数:

bash
-XX:+UseSerialGC  # 新生代使用 Serial,老年代使用 Serial Old

2.3.2 Parallel Scavenge 收集器

特点:多线程收集器,吞吐量优先

适用场景:后台计算

┌─────────────────────────────────┐
│         应用线程暂停             │
├─────────────────────────────────┤
│  GC 线程1  │  GC 线程2  │ ...   │
└─────────────────────────────────┘

运行参数:

bash
-XX:+UseParallelGC  # 新生代使用 Parallel Scavenge
-XX:+UseParallelOldGC  # 老年代使用 Parallel Old
-XX:MaxGCPauseMillis=200  # 最大 GC 停顿时间
-XX:GCTimeRatio=19  # GC 时间占比

2.3.3 CMS 收集器(Concurrent Mark Sweep)

特点:低延迟收集器,并发标记和清除

适用场景:Web 应用

过程:

初始标记(STW)

并发标记

重新标记(STW)

并发清除

┌─────────────────────────────────┐
│  应用线程  │  应用线程  │  应用线程 │
├─────────────────────────────────┤
│  初始标记  │  并发标记  │ 重新标记 │ 并发清除
│   (STW)   │           │  (STW)  │
└─────────────────────────────────┘

运行参数:

bash
-XX:+UseConcMarkSweepGC  # 使用 CMS 收集器
-XX:CMSInitiatingOccupancyFraction=75  # 老年代使用率达到 75% 时触发 CMS
-XX:+UseCMSInitiatingOccupancyOnly  # 只使用设定的阈值

缺点:

  • CPU 敏感
  • 无法处理浮动垃圾
  • 内存碎片

2.3.4 G1 收集器(Garbage-First)

特点:面向全堆,可预测的停顿时间

适用场景:大堆、低延迟

核心概念:

  • Region:将堆划分为多个大小相等的 Region
  • Eden、Survivor、Old、Humongous:不同类型的 Region
┌─────┬─────┬─────┬─────┬─────┐
│ Eden│ S0  │ Old │ Old │  H  │
├─────┼─────┼─────┼─────┼─────┤
│ Old │ Old │  H  │ Eden│ S1  │
└─────┴─────┴─────┴─────┴─────┘

过程:

初始标记(STW)

并发标记

最终标记(STW)

筛选回收(STW)

运行参数:

bash
-XX:+UseG1GC  # 使用 G1 收集器
-XX:MaxGCPauseMillis=200  # 最大 GC 停顿时间
-XX:G1HeapRegionSize=4m  # Region 大小
-XX:InitiatingHeapOccupancyPercent=45  # 堆使用率达到 45% 时触发 G1

2.4 对象分配策略

2.4.1 TLAB(Thread Local Allocation Buffer)

每个线程在 Eden 区分配一个私有的内存缓冲区,减少锁竞争。

┌─────────────────────────────────┐
│            Eden 区               │
├──────────┬──────────┬───────────┤
│ TLAB 1   │ TLAB 2   │ TLAB 3    │
│ (线程1)  │ (线程2)  │ (线程3)   │
└──────────┴──────────┴───────────┘

2.4.2 优先在 Eden 区分配

对象优先在 Eden 区分配,Minor GC 后存活的对象进入 Survivor 区,经过多次 Minor GC 后进入老年代。

Eden → Survivor → Old

2.4.3 大对象直接进入老年代

大对象(如长数组、长字符串)直接进入老年代,避免在 Eden 和 Survivor 之间频繁复制。

bash
-XX:PretenureSizeThreshold=1m  # 大于 1MB 的对象直接进入老年代

2.4.4 长期存活的对象进入老年代

对象在 Survivor 区每经过一次 Minor GC,年龄加 1。当年龄达到阈值(默认 15)时,进入老年代。

bash
-XX:MaxTenuringThreshold=15  # 最大年龄阈值

三、重要知识点

3.1 GC 日志分析

bash
# 开启 GC 日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log

# 使用 GCViewer 分析 GC 日志
# 下载 GCViewer,打开 gc.log 文件

3.2 垃圾收集器选择

收集器新生代老年代特点适用场景
SerialSerialSerial Old单线程客户端模式
ParallelParallel ScavengeParallel Old吞吐量优先后台计算
CMSParNewCMS低延迟Web 应用
G1G1G1可预测停顿大堆、低延迟

3.3 GC 调优原则

减少 Minor GC 频率:增大新生代

减少 Full GC 频率:增大老年代、调整 CMS/G1 触发阈值

减少 GC 停顿时间:使用 G1、调整停顿时间目标

提高吞吐量:使用 Parallel 收集器


四、常见误区与难点

4.1 误区一:"GC 越少越好"

GC 是必要的,过多的 GC 会影响性能,但完全没有 GC 会导致内存溢出。

4.2 误区二:"G1 总是比 CMS 好"

G1 和 CMS 各有优缺点,需要根据实际场景选择。G1 适合大堆,CMS 适合小堆。

4.3 难点:理解 G1 的 Region 机制

G1 将堆划分为多个 Region,每个 Region 可以是 Eden、Survivor、Old 或 Humongous。理解 Region 的分配和回收机制是掌握 G1 的关键。

4.4 难点:理解并发标记和重新标记

CMS 和 G1 都使用并发标记,但需要重新标记来修正并发标记期间的变动。理解 STW(Stop-The-World)的时机和影响对于调优至关重要。


五、实践应用

5.1 查看 GC 信息

bash
# 查看 GC 情况
jstat -gcutil <pid> 1000

# 查看 GC 原因
jstat -gccause <pid>

# 查看 GC 日志
tail -f gc.log

5.2 调优 GC 参数

bash
# G1 收集器调优
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=4m
-XX:InitiatingHeapOccupancyPercent=45
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=4

# CMS 收集器调优
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=4

5.3 分析 GC 日志

bash
# 使用 GCViewer 分析
# 下载 GCViewer:https://github.com/chewiebug/GCViewer

# 使用 GCEasy 在线分析
# https://gceasy.io/

六、本章小结

第3章讲解了垃圾收集器与内存分配策略:

引用计数法:无法解决循环引用

可达性分析:从 GC Roots 搜索存活对象

垃圾收集算法:标记-清除、标记-复制、标记-整理、分代收集

Serial 收集器:单线程,客户端模式

Parallel 收集器:多线程,吞吐量优先

CMS 收集器:低延迟,并发标记和清除

G1 收集器:面向全堆,可预测的停顿时间

对象分配策略:TLAB、优先在 Eden 区分配、大对象直接进入老年代

后续章节将讲解虚拟机性能监控与调优工具。


3/13垃圾收集
0%