引言:GC 不是“黑盒”,而是性能的关键钥匙

Java 的自动内存管理极大提升了开发效率,但“自动”不等于“无脑”。在高并发、低延迟、大内存场景下,垃圾回收器的选择与调优直接决定系统生死


一、JVM 运行时数据区:GC 的战场地图

首先纠正一个常见误区:GC 不只发生在堆!

JVM 内存分为 线程共享区线程私有区

JVM Memory

Thread-Shared

Thread-Local

Heap
对象实例、数组

Metaspace
类元数据(JDK 8+)

Runtime Constant Pool
字符串常量、final 常量

Java Virtual Machine Stack
局部变量、方法帧

Native Method Stack

Program Counter Register

关键结论

  • 堆(Heap) 是 GC 主战场(95%+ 回收发生于此)
  • Metaspace 也会 GC(当类卸载时)
  • 栈/PC寄存器 生命周期随线程结束而释放,无需 GC

堆的分代结构(以 G1 为例)

当然可以!以下是修正后、可直接渲染的图表示例,使用标准 Mermaid 语法(兼容主流 Markdown 编辑器如 Typora、VS Code 插件、Obsidian、GitLab/GitHub 配合插件等),并优化了传统分代结构的 ASCII 图,使其更清晰美观。


传统分代模型(Serial / Parallel / CMS)

+---------------------------------------------------+
|                Old Generation (老年代)             |
|                                                   |
|                                                   |
+------------------+------------------+-------------+
|     Eden         |   Survivor S0    | Survivor S1 |
|   (伊甸区)       |   (幸存者区)     | (幸存者区)  |
+------------------+------------------+-------------+
|               Young Generation (新生代)           |
+---------------------------------------------------+

💡 说明

  • 新生代 = Eden + S0 + S1(通常比例为 8:1:1)
  • 对象首先在 Eden 分配
  • 经历多次 Young GC 后仍存活的对象晋升至老年代

G1 的 Region 化堆结构(逻辑分代,物理不分代)

G1 将整个堆划分为 多个大小相等的 Region(默认 2048 个),每个 Region 可动态扮演 Eden、Survivor、Old 或 Humongous 角色。

55% 35% 5% 5% G1 堆中 Region 类型分布示例 Eden Regions Survivor Regions Old Regions Humongous Regions

提示

  • 实际比例随 GC 过程动态变化
  • Humongous Region:用于存储超过 Region 50% 的大对象(如大数组)
  • 每个 Region 大小 = HeapSize / 2048,范围 1MB ~ 32MB,可通过 -XX:G1HeapRegionSize 手动指定(必须是 2 的幂)

补充:G1 Region 布局示意图(文本版)

+----+----+----+----+----+----+----+----+----+----+
| E  | E  | E  | S  | O  | O  | H  | O  | E  | O  |
+----+----+----+----+----+----+----+----+----+----+
| E  | O  | O  | O  | S  | O  | O  | O  | O  | O  |
+----+----+----+----+----+----+----+----+----+----+
| O  | O  | H  | E  | E  | O  | O  | O  | O  | O  |
+----+----+----+----+----+----+----+----+----+----+

E = Eden Region  
S = Survivor Region  
O = Old Region  
H = Humongous Region(可能跨多个连续 Region)

这种灵活的布局使 G1 能够:

  • 优先回收垃圾最多的 Region(Garbage-First)

  • 避免全堆 STW

  • 实现可预测的停顿时间


二、对象“死亡”的判定:不止是引用断开

2.1 可达性分析(Reachability Analysis)

JVM 从 GC Roots 出发,构建引用链。不可达对象 = 可回收。

GC Roots 包括:
  • 虚拟机栈中的局部变量表引用
  • 方法区中静态变量引用
  • 方法区中常量引用(如 "hello" 字符串字面量)
  • 本地方法栈中 JNI 引用
  • 活跃线程本身也是 Root
public class GcRootsExample {
    static Object staticRef = new Object(); // Root 1

    public void method() {
        Object localRef = new Object(); // Root 2(方法执行期间)
        Thread thread = new Thread(() -> {}); // Root 3(若 start() 则成为活跃线程)
        String s = "literal"; // Root 4(字符串常量池)
    }
}

🔥 注意:即使两个对象互相引用(A → B → A),但若整体不可达,仍会被回收。

2.2 引用类型:让 GC 更“智能”

Java 提供四种引用强度,控制对象回收时机:

引用类型 回收条件 典型用途
强引用(Strong) 永不回收(除非不可达) Object obj = new Object()
软引用(Soft) 内存不足时回收 缓存(如图片缓存)
弱引用(Weak) 下次 GC 必回收 WeakHashMap、监听器清理
虚引用(Phantom) 无法阻止回收,仅用于通知 资源清理(配合 ReferenceQueue)
实验:观察软引用行为
import java.lang.ref.SoftReference;

public class SoftRefDemo {
    public static void main(String[] args) {
        SoftReference<byte[]> ref = new SoftReference<>(new byte[100 * 1024 * 1024]); // 100MB
        System.out.println("Before GC: " + (ref.get() != null));

        // 尝试触发内存压力
        try {
            byte[] pressure = new byte[150 * 1024 * 1024]; // 若堆小,会触发回收 soft ref
        } catch (OutOfMemoryError ignored) {}

        System.gc();
        System.out.println("After GC: " + (ref.get() != null)); // 可能为 false
    }
}

启动参数:

java -Xmx200m -XX:+PrintGCDetails SoftRefDemo

三、垃圾回收算法:从理论到工程实现

3.1 标记-清除(Mark-Sweep)

  • 步骤:
    1. 从 GC Roots 标记所有存活对象
    2. 遍历整个堆,清除未标记对象
  • 缺点:
    • 内存碎片化(分配大对象时可能失败,即使总空闲足够)
    • 效率随堆增大而下降

3.2 复制算法(Copying)

  • 将内存分为 FromTo 两块
  • GC 时,将 From 中存活对象复制到 To,然后交换角色
  • 优点:无碎片、实现简单
  • 代价:50% 空间浪费

📌 新生代为何适合复制算法?
IBM 研究表明:98% 的对象活不过第一轮 GC。因此 Survivor 区通常很小(如 Eden:S0:S1 = 8:1:1)。

3.3 标记-整理(Mark-Compact)

  • 标记后,将存活对象向一端移动,清理边界外空间
  • 解决碎片问题,但移动成本高
  • 用于老年代

3.4 分代收集(Generational Collection)

基于 弱分代假说(Weak Generational Hypothesis)

  1. 大多数对象朝生夕死
  2. 存活越久的对象,越可能继续存活

因此:

  • Young GC:高频、快速(复制算法)
  • Old GC:低频、慢(标记-整理/清除)

四、主流垃圾回收器深度对比(含演进路线)

回收器演进时间线

JDK 1.0 ~ 1.3 Serial GC : 单线程,STW JDK 1.4 ~ 6 Parallel GC : 吞吐量优先 CMS : 并发低延迟(但碎片+退化 Full GC) JDK 7 ~ 8 G1 : 可预测停顿,Region JDK 11+ ZGC : <10ms 停顿,着色指针 Shenandoah : 类似 ZGC,Red Hat 主导 JVM GC 器演进史

九大回收器特性矩阵

回收器 年代 算法 并发 STW 目标 状态
Serial Y+O 复制 + 标记整理 简单/小内存 保留
ParNew Y 复制 CMS 搭配 保留
Parallel Scavenge Y 复制 吞吐量优先 默认(JDK 8)
Parallel Old O 标记整理 吞吐量 保留
CMS O 标记清除 ✅(大部分) 低延迟 JDK 14+ 移除
G1 Y+O 复制 + 标记整理 ✅(部分) 可控 平衡吞吐与延迟 JDK 9+ 默认
ZGC 全堆 着色指针 + Load Barrier <10ms 超低延迟 JDK 11+(实验),15+ 生产
Shenandoah 全堆 Brooks Pointer + SATB <10ms 超低延迟 OpenJDK 12+
Epsilon 无 GC 性能测试/短生命周期 JDK 11+

💡 选择建议

  • Web 服务(响应敏感)→ G1 / ZGC
  • 批处理(吞吐优先)→ Parallel GC
  • 容器化小内存 → Serial

五、G1 垃圾回收器:现代 GC 的集大成者

5.1 G1 的核心设计

  • Region 化堆:打破物理分代,逻辑分代
  • Remembered Sets (RSet):记录跨 Region 引用,避免全堆扫描
  • Collection Set (CSet):每次 GC 选择垃圾最多的 Region 回收
  • 并发标记:基于 SATB(Snapshot-At-The-Beginning) 算法

5.2 G1 的 GC 周期

Initial Mark
STW, 标记 GC Roots

Root Region Scan
并发, 扫描 Survivor 引用

Concurrent Mark
并发标记整个堆

Remark
STW, 处理变更

Cleanup
STW, 统计 Region 垃圾比例

是否需要 Mixed GC?

Mixed GC
回收 Young + 部分 Old

Young GC

5.3 关键参数调优

# 基础配置
-XX:+UseG1GC
-Xms8g -Xmx8g

# 控制停顿时间(目标,非保证)
-XX:MaxGCPauseMillis=100

# 控制 Mixed GC 行为
-XX:G1MixedGCCountTarget=8          # Mixed GC 轮数
-XX:G1HeapWastePercent=5            # 允许的堆浪费比例
-XX:G1MixedGCLiveThresholdPercent=85 # Region 存活 >85% 则跳过

# 避免 Humongous 对象问题
-XX:G1HeapRegionSize=32m            # 若常分配 >16MB 对象

六、ZGC:面向未来的超低延迟 GC

6.1 核心技术

  • 着色指针(Colored Pointers)
    在 64 位指针中嵌入 metadata bits(如 mark、remap),无需额外对象头
  • Load Barrier
    在读取对象时插入屏障代码,实现并发重定位
  • 多映射(Multi-mapping)
    同一物理内存映射到多个虚拟地址,支持并发访问旧/新地址

6.2 ZGC 停顿时间 vs 堆大小

堆大小 Max Pause Time
128MB ~0.5 ms
16GB ~1.5 ms
128GB ~2.0 ms

适用场景:金融交易、实时游戏、AI 推理等对延迟极度敏感的系统。

启动方式:

java -XX:+UseZGC -Xmx64g MyApp

七、实战:GC 日志分析与调优

7.1 开启详细 GC 日志(JDK 9+)

java \
  -Xlog:gc*,gc+age=trace,safepoint:file=gc.log:time,tags \
  -XX:+UseG1GC \
  -Xmx4g \
  MyApp

7.2 日志解读示例(G1 Young GC)

[2026-01-26T21:10:00.123+0800][gc,start     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[2026-01-26T21:10:00.125+0800][gc,phases   ] GC(0)   Pre Evacuate Collection Set: 0.1ms
[2026-01-26T21:10:00.130+0800][gc,phases   ] GC(0)   Evacuate Collection Set: 4.8ms
[2026-01-26T21:10:00.131+0800][gc,heap     ] GC(0) Eden regions: 120->0(120)
[2026-01-26T21:10:00.131+0800][gc,heap     ] GC(0) Survivor regions: 8->10(10)
[2026-01-26T21:10:00.131+0800][gc,heap     ] GC(0) Old regions: 200->202
[2026-01-26T21:10:00.131+0800][gc          ] GC(0) Pause Young (Normal) 328M->212M(4096M) 8.234ms
  • Evacuate 时间过长? → 可能 Survivor 区太小,对象过早晋升
  • Old 区增长快? → 检查是否有内存泄漏或缓存未清理

7.3 调优 Checklist

监控指标

  • Young GC 频率(理想:几秒一次)
  • Old 区增长率(应缓慢)
  • Full GC 次数(生产环境应为 0)

调优步骤

  1. 确定业务目标(吞吐?延迟?)
  2. 选择合适 GC 器
  3. 设置合理堆大小(避免频繁 GC)
  4. 调整分代比例(如 -XX:NewRatio
  5. 监控 + 压测 + 迭代

八、总结:GC 调优不是玄学,而是科学

维度 关键点
内存模型 堆是主战场,但 Metaspace 也需关注
对象生死 可达性分析 + 四种引用类型
算法选择 新生代用复制,老年代用整理/并发
回收器选型 G1 通用,ZGC 超低延迟,Parallel 吞吐优先
调优核心 监控驱动,而非盲目调参

🌟 终极建议
不要等到线上 Full GC 才学习 GC
从今天开始,在测试环境开启 GC 日志,理解你的应用内存行为。


附录:推荐工具

  • GC 日志分析GCEasy.io
  • 堆转储分析:Eclipse MAT、VisualVM
  • 实时监控:Prometheus + JMX Exporter + Grafana
  • 压测工具:JMH(微基准)、wrk(HTTP)

如果你喜欢这篇文章,欢迎关注我的技术公众号「程序员技术实录」
💌 每周更新:系统设计|云原生|…

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐