垃圾回收
→ 返回 Java 基础
→ 对象主题:实例的存活、引用、晋升与回收以本文为权威;布局与 new 见 JVM;总览 对象
GC(Garbage Collection)负责堆上对象实例的自动回收;Metaspace 中的类元数据由类卸载机制处理(常伴随 Full GC)。堆/非堆划分见 JVM。
对象存活判断
对象生命周期简图与索引:生命周期与 GC(摘要)。本节只讲哪些实例会被 GC 认为可回收。
引用计数法
每个对象维护引用计数,为 0 时回收。无法解决循环引用,Java 未采用。
可达性分析(Java 采用)
从 GC Roots 出发遍历引用链,不可达的对象可被回收(并非立即回收,会经历至少一次标记)。
GC Roots 包括:
| 类型 | 示例 |
|---|---|
| 虚拟机栈中局部变量 | 方法参数、局部对象引用 |
| 静态变量 | static Object ref 指向的堆对象 |
| 常量 | 运行时常量池中的引用 |
| JNI 句柄 | native 方法持有的 Java 对象 |
| 同步锁持有对象 | 被 monitorenter 关联的对象(仍可达,故不回收;锁语义见 JUC) |
| JVM 内部引用 | 系统类加载器、异常对象等 |
两次标记与 finalize
- 第一次标记:不可达对象
- 筛选:是否有必要执行
finalize()(对象未覆盖或已执行过则跳过) - finalize 后仍不可达 → 第二次标记,真正回收
finalize()已废弃(Java 9+),生产代码勿依赖;资源释放用try-with-resources/Cleaner。
引用类型
| 类型 | 回收时机 | 典型用途 |
|---|---|---|
| 强引用 | OOM 也不回收 | Object o = new Object() |
软引用 SoftReference | 内存不足时回收 | 图片缓存 |
弱引用 WeakReference | 下次 GC 必回收 | WeakHashMap、ThreadLocal 键 |
虚引用 PhantomReference | 随时回收,无法 get() | 堆外内存回收跟踪、DirectBuffer 清理 |
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
WeakReference<Object> weakRef = new WeakReference<>(new Object());
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);堆内存分区
堆(Heap,-Xms / -Xmx)
├── 新生代(Young Generation)约 1/3
│ ├── Eden(约 8/10) ← 新对象分配区
│ ├── Survivor From(约 1/10)
│ └── Survivor To(约 1/10) ← Minor GC 后互换角色
└── 老年代(Old Generation)约 2/3
| 参数 | 作用 |
|---|---|
-XX:NewRatio=2 | 老年代:新生代 = 2:1 |
-XX:SurvivorRatio=8 | Eden:单个 Survivor = 8:1 |
-Xmn512m | 直接指定新生代大小 |
不在堆中:类元数据在 Metaspace(见 JVM · Metaspace),JIT 机器码在 Code Cache。堆结构、TLAB、G1 Region 详见 JVM · 堆。
TLAB(线程本地分配)
分配路径(逃逸分析、Eden、大对象)的权威说明在 对象分配路径;此处只说明 TLAB 与 GC 分区的关系。
多线程在 Eden 分配对象时,JVM 为每个线程划一小块 TLAB(Thread Local Allocation Buffer),在缓冲内分配无需 CAS,减少竞争。
Eden
├── TLAB(线程 1) ← 优先在此 bump-the-pointer 分配
├── TLAB(线程 2)
└── 共享 Eden 剩余区 ← TLAB 用尽再 CAS 分配
- 默认开启:
-XX:+UseTLAB - 大对象可能绕过 TLAB 直接在 Eden 或老年代分配
对象晋升与老年代
标准晋升流程
- 新对象在 Eden 分配(TLAB)
- Eden 满 → Minor GC(Young GC):存活对象复制到 To Survivor,From 清空;年龄 = 0 的对象年龄变为 1
- 再次 Minor GC:Eden + From 存活对象复制到 To,在 Survivor 中熬过 GC 的对象 年龄 +1
- 晋升老年代条件(满足其一):
- 年龄 ≥
-XX:MaxTenuringThreshold(默认 15,与 Mark Word 4 位年龄一致) - 动态年龄判定:同年龄对象大小超过 Survivor 一半,≥ 该年龄的对象直接晋升
- Survivor 放不下存活对象
- 大对象:超过
-XX:PretenureSizeThreshold(仅 Serial / ParNew 等有效;G1 用G1HeapRegionSize)→ 直接进老年代
- 年龄 ≥
空间分配担保(Minor GC 前)
老年代需预留空间容纳 Survivor 所有存活对象(历史晋升平均值)。担保失败可能提前触发 Full GC。
GC 类型与术语
| 名称 | 范围 | 说明 |
|---|---|---|
| Minor GC / Young GC | 新生代 | 频繁、较快;Eden + Survivor |
| Major GC | 老年代 | 有时泛指 Full GC |
| Full GC | 整堆 + 方法区/元空间 | STW 长;常回收 Metaspace 无用类 |
| Mixed GC | G1:部分年轻 + 部分老 Region | G1 特有,控制停顿 |
典型分代收集器:
对象分配 → Eden
↓ Eden 满
Minor GC(复制 → Survivor)
↓ 年龄 / 大对象 / Survivor 满
晋升老年代
↓ 老年代满
Major / Full GC(标记-整理 / 清除)
Safepoint 与 STW
- Safepoint:线程可安全暂停以执行 GC 的点(方法调用、循环回边等)
- Stop-The-World(STW):GC 阶段暂停所有应用线程
- 并发 GC(CMS/G1/ZGC 部分阶段):与用户线程并发,仍有短 STW(初始标记、最终标记等)
垃圾收集算法
| 算法 | 原理 | 适用 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记后清除垃圾 | CMS 老年代 | 碎片 |
| 标记-整理 | 存活对象移到一端 | Serial Old、Parallel Old | 移动开销 |
| 复制 | 存活对象复制到另一空间 | 新生代(Eden→Survivor) | 需预留空间 |
| 分代收集 | 年轻代复制 + 老年代整理 | 传统 HotSpot | 实现复杂 |
垃圾收集器
收集器组合(JDK 8 经典)
| 新生代 | 老年代 | 说明 |
|---|---|---|
| Serial | Serial Old | 单线程,Client |
| ParNew | CMS | CMS 已移除 |
| Parallel Scavenge | Parallel Old | 吞吐量优先,JDK 8 Server 默认 |
| G1 | G1 | JDK 9+ 默认 |
Serial / Serial Old
单线程 STW。-XX:+UseSerialGC。
Parallel Scavenge / Parallel Old
吞吐量优先(-XX:+UseParallelGC):
-XX:MaxGCPauseMillis=500 # 尽力控制停顿(与吞吐量权衡)
-XX:GCTimeRatio=19 # 吞吐量 = 1/(1+19) = 5% 时间用于 GC
-XX:ParallelGCThreads=N适合批处理、离线计算。
CMS(已废弃)
并发标记清除,Java 14 移除。曾用 -XX:+UseConcMarkSweepGC。
初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除
缺点:碎片、浮动垃圾、Concurrent Mode Failure 退化为 Serial Old Full GC。
G1(Garbage First)
JDK 9+ 默认。堆划分为等大小 Region(1~32 MB,-XX:G1HeapRegionSize),每 Region 角色可为 Eden / Survivor / Old / Humongous(大对象)。
回收流程:
1. Young GC 回收所有年轻 Region(Eden + Survivor)
2. 并发标记周期 当堆占用 ≥ InitiatingHeapOccupancyPercent(默认 45%)
3. Mixed GC 回收年轻 Region + 部分垃圾最多的 Old Region
4. Full GC 并发标记失败 / Evacuation 失败时退化(应尽量避免)
关键参数:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=4m
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1ReservePercent=10ZGC(Z Garbage Collector)
ZGC 是 Oracle 开发的超低延迟收集器,目标是把 STW 停顿控制在 1 ms 以内,且停顿时间与堆大小、存活对象数量基本无关(可支撑 TB 级堆)。JDK 11 实验引入,JDK 15+ 生产可用;JDK 21+ 推荐开启分代 ZGC。
设计目标与适用场景
| 特性 | 说明 |
|---|---|
| 停顿 | 亚毫秒级 STW,不随堆从 GB 涨到 TB 而线性变长 |
| 堆范围 | 约 8 MB ~ 16 TB(64 位) |
| 整理 | 并发移动对象,无碎片问题(非标记-清除) |
| 典型场景 | 交易系统、实时服务、大堆低延迟、容器内敏感 P99 |
不适合:纯批处理、只关心吞吐量不关心停顿(Parallel GC 可能更省 CPU)。
与 G1 对比
| 维度 | G1 | ZGC |
|---|---|---|
| 停顿目标 | -XX:MaxGCPauseMillis=200(尽力而为) | 固定极短 STW(< 1 ms 级) |
| 大堆表现 | 堆越大,单次停顿往往越长 | 停顿与堆大小弱相关 |
| 核心技术 | Region + 写屏障(SATB) | 着色指针 + 读屏障 + 并发 relocation |
| 分代 | 物理 Region 分 Eden/S/Old | JDK 20 前不分代;JDK 21+ 可选分代 |
| 默认 | JDK 9+ 默认 | 需 -XX:+UseZGC 显式开启 |
核心机制
1. 着色指针(Colored Pointers)
64 位指针高位拿出若干 bit 存 GC 元数据(如 Marked0、Marked1、Remapped、Finalizable),在指针本身携带状态,减少额外对象头开销。
64 位地址(概念示意)
┌─────────────── 着色位(元数据) ───────────────┬──── 实际地址 ────┐
│ Finalizable / Remapped / Marked1 / Marked0 … │ 指向堆内对象 │
└────────────────────────────────────────────────┴──────────────────┘
仅 64 位、虚拟地址空间充足的平台可用(x64、aarch64)。
2. 读屏障(Load Barrier)
应用线程从堆读引用时插入屏障:若指针颜色「过期」,则修正为最新地址。
线程执行:Object o = field; // 读引用
│
▼
Load Barrier 检查着色位
│
├── 已 Remapped → 直接使用
└── 未更新 → 修正指针 → 继续执行
这使 对象正在被并发移动 时,Java 线程仍可运行——与 G1 主要用写屏障不同,ZGC 依赖读屏障保证看到正确引用。
3. 并发 relocation(并发整理)
标记完成后,ZGC 并发把存活对象搬到新位置并更新引用,无需像 CMS 那样留下碎片,也无需长时间 STW 整理。
堆布局(Page)
ZGC 将堆划分为多种 Page(类似 Region,但规则不同):
| Page 类型 | 说明 |
|---|---|
| Small | 固定 2 MB,小对象分配 |
| Medium | 固定 32 MB,中等对象 |
| Large | 按对象大小单独分配,一个大对象一页 |
GC 周期(简化)
ZGC 一次完整周期大致如下(多数阶段与用户线程并发):
Pause Mark Start (STW,极短)
│
▼
Concurrent Mark 并发标记存活对象
│
▼
Pause Mark End (STW,极短)
│
▼
Concurrent Prepare for Relocate / 处理弱引用等
│
▼
Pause Relocate Start (STW,极短)
│
▼
Concurrent Relocate 并发移动对象,读屏障修正引用
│
▼
周期结束,等待下次触发
日志里常见 GC(0) Pause Mark Start、Pause Relocate Start 等,STW 段通常远短于 G1 的 Mixed GC。
分代 ZGC(JDK 21+)
早期 ZGC 不分代,短生命周期对象也要走完整周期,高分配速率时 CPU 开销偏大。
JDK 21 引入 Generational ZGC(-XX:+ZGenerational),增加年轻代回收短命对象,多数 Web / 微服务场景更省 CPU;JDK 23+ 分代 ZGC 成为默认行为(以实际 JDK 版本为准)。
分代 ZGC(概念)
新对象 → 年轻代(频繁、轻量 Young GC)
↓ 存活对象
老年代 → 仍用 ZGC 完整并发周期整理
常用参数
-XX:+UseZGC # 启用 ZGC
-Xms4g -Xmx4g # 建议相等;ZGC 堆扩缩行为与 G1 不同
-XX:SoftMaxHeapSize=3500m # 软上限,JVM 尽量不超过(可配合 -Xmx 留余量)
# JDK 21+ 分代(推荐)
-XX:+ZGenerational
# 并发 / 并行线程
-XX:ConcGCThreads=2 # 并发 GC 线程(默认约 CPU/8)
-XX:ParallelGCThreads=8 # STW 阶段并行线程
# 主动 GC 间隔(秒,0=禁用)
-XX:ZCollectionInterval=5
# 内存归还 OS(空闲 Page 延迟 uncommit)
-XX:ZUncommitDelay=300
# GC 日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags生产模板见 启动参数调优 · ZGC。
监控与排查
jstat -gcutil <pid> 1000 # 各分区利用率
jcmd <pid> GC.heap_info
-Xlog:gc* # 关注 Pause Mark/Relocate 耗时| 现象 | 可能原因 |
|---|---|
| CPU 占用偏高 | 高分配速率 + 未开分代 ZGC;可适当 -XX:ConcGCThreads |
| 堆涨但业务无泄漏 | ZGC 延迟 uncommit,或 SoftMaxHeapSize 设得高 |
| 停顿仍 > 1ms | 极端根集、JFR 看 Safepoint;核对是否真在用 ZGC |
启用示例(JDK 21 低延迟服务)
java -XX:+UseZGC \
-XX:+ZGenerational \
-Xms4g -Xmx4g \
-XX:SoftMaxHeapSize=3800m \
-XX:+HeapDumpOnOutOfMemoryError \
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10m \
-jar app.jar堆布局与 ZGC 在 JVM · 堆 一节有简要交叉说明。
Shenandoah
OpenJDK 包含,目标类似 ZGC,低停顿、并发整理,适合大堆低延迟。
Metaspace 与类卸载
堆 GC 不直接管理 Metaspace;类卸载在 Full GC 等时机由 JVM 判定:
ClassLoader 不可达 + 该类所有实例已回收 + 无 JNI 引用
→ 卸载类 → 释放 Metaspace / Compressed Class Space
| 场景 | 风险 |
|---|---|
| Spring DevTools 热重启 | 多次类加载 |
| CGLib / JDK Proxy 动态类 | Metaspace 涨 |
| Groovy、JSP | 类数量大 |
| OSGi、插件化 | ClassLoader 泄漏 → Metaspace OOM |
Metaspace 细节与 CCS 见 JVM · Metaspace、Compressed Class Space。
常用 JVM 参数
# 堆
-Xms2g -Xmx2g # 建议相等,避免扩缩
-Xmn512m
-XX:NewRatio=2
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
# 收集器
-XX:+UseG1GC
-XX:+UseZGC
-XX:+UseParallelGC
# G1
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=4m
# Metaspace
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
# GC 日志(JDK 9+)
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=20m
# OOM
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof完整清单见 启动参数调优。
GC 调优思路
- 明确目标:吞吐量(Parallel)vs 停顿(G1/ZGC)
- 减少 Full GC:合理新生代比例;避免大对象打满老年代;排查 Metaspace 类泄漏
- 读 GC 日志:关注 停顿时间、频率、回收前后占用、晋升失败、to-space exhausted
- 堆 dump:
jmap -dump:live+ MAT 看 dominator tree、泄漏路径 - Native Memory:Metaspace/CCS/Code Cache 问题用
jcmd VM.native_memory summary
常用命令
jstat -gcutil <pid> 1000 # 各区利用率
jstat -gccause <pid> # 最近一次 GC 原因
jcmd <pid> GC.heap_info
jcmd <pid> VM.metaspace
jcmd <pid> VM.native_memory summary常见问题
| 问题 | 原因 | 排查 |
|---|---|---|
Java heap space | 堆不足或泄漏 | heap dump + MAT |
Metaspace | 类加载过多 / ClassLoader 泄漏 | jcmd VM.metaspace、查动态代理 |
Compressed class space | CCS 上限过小或类过多 | 增大 CompressedClassSpaceSize |
CodeCache is full | JIT 编译过多 | 增大 ReservedCodeCacheSize,见 Code Cache |
| Full GC 频繁 | 老年代满、Metaspace 触发、晋升过快 | GC 日志、调新生代 / 堆 |
| Minor GC 过频 | Eden 过小 | 增大新生代或 -Xmx |
| GC 停顿过长 | Serial Old、CMS FLS、G1 大堆 Mixed GC | 换 ZGC(见 ZGC 小节)或 Shenandoah |
to-space exhausted | Survivor 放不下 | 调 SurvivorRatio 或 MaxTenuringThreshold |
concurrent mode failure | CMS 期间老年代满(历史) | 已弃用 CMS,换 G1 |
收集器选型简表
| 场景 | 推荐 |
|---|---|
| 单核 / 小堆客户端 | Serial |
| 批处理、大吞吐量 | Parallel GC |
| 通用 Web 服务(JDK 9+) | G1 |
| 超低延迟、大堆 | ZGC / Shenandoah |
| 容器内 Java | G1 或 ZGC + 容器感知 -XX:MaxRAMPercentage |