深入 JVM 垃圾回收机制:从内存模型到 GC 调优实战
引言:GC 不是“黑盒”,而是性能的关键钥匙
Java 的自动内存管理极大提升了开发效率,但“自动”不等于“无脑”。在高并发、低延迟、大内存场景下,垃圾回收器的选择与调优直接决定系统生死。
一、JVM 运行时数据区:GC 的战场地图
首先纠正一个常见误区:GC 不只发生在堆!
JVM 内存分为 线程共享区 与 线程私有区:
✅ 关键结论:
- 堆(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 角色。
✅ 提示:
- 实际比例随 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)
- 步骤:
- 从 GC Roots 标记所有存活对象
- 遍历整个堆,清除未标记对象
- 缺点:
- 内存碎片化(分配大对象时可能失败,即使总空闲足够)
- 效率随堆增大而下降
3.2 复制算法(Copying)
- 将内存分为 From 和 To 两块
- 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):
- 大多数对象朝生夕死
- 存活越久的对象,越可能继续存活
因此:
- Young GC:高频、快速(复制算法)
- Old 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 周期
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)
✅ 调优步骤:
- 确定业务目标(吞吐?延迟?)
- 选择合适 GC 器
- 设置合理堆大小(避免频繁 GC)
- 调整分代比例(如
-XX:NewRatio) - 监控 + 压测 + 迭代
八、总结:GC 调优不是玄学,而是科学
| 维度 | 关键点 |
|---|---|
| 内存模型 | 堆是主战场,但 Metaspace 也需关注 |
| 对象生死 | 可达性分析 + 四种引用类型 |
| 算法选择 | 新生代用复制,老年代用整理/并发 |
| 回收器选型 | G1 通用,ZGC 超低延迟,Parallel 吞吐优先 |
| 调优核心 | 监控驱动,而非盲目调参 |
🌟 终极建议:
不要等到线上 Full GC 才学习 GC。
从今天开始,在测试环境开启 GC 日志,理解你的应用内存行为。
附录:推荐工具
- GC 日志分析:GCEasy.io
- 堆转储分析:Eclipse MAT、VisualVM
- 实时监控:Prometheus + JMX Exporter + Grafana
- 压测工具:JMH(微基准)、wrk(HTTP)
如果你喜欢这篇文章,欢迎关注我的技术公众号「程序员技术实录」
💌 每周更新:系统设计|云原生|…
更多推荐



所有评论(0)