垃圾回收

返回 Java 基础
→ 对象主题:实例的存活、引用、晋升与回收以本文为权威;布局与 newJVM;总览 对象

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

  1. 第一次标记:不可达对象
  2. 筛选:是否有必要执行 finalize()(对象未覆盖或已执行过则跳过)
  3. 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=8Eden:单个 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 或老年代分配

对象晋升与老年代

标准晋升流程

  1. 新对象在 Eden 分配(TLAB)
  2. Eden 满Minor GC(Young GC):存活对象复制到 To Survivor,From 清空;年龄 = 0 的对象年龄变为 1
  3. 再次 Minor GC:Eden + From 存活对象复制到 To,在 Survivor 中熬过 GC 的对象 年龄 +1
  4. 晋升老年代条件(满足其一):
    • 年龄 ≥ -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 GCG1:部分年轻 + 部分老 RegionG1 特有,控制停顿
典型分代收集器:

  对象分配 → 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 经典)

新生代老年代说明
SerialSerial Old单线程,Client
ParNewCMSCMS 已移除
Parallel ScavengeParallel Old吞吐量优先,JDK 8 Server 默认
G1G1JDK 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=10

ZGC(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 对比

维度G1ZGC
停顿目标-XX:MaxGCPauseMillis=200(尽力而为)固定极短 STW(< 1 ms 级)
大堆表现堆越大,单次停顿往往越长停顿与堆大小弱相关
核心技术Region + 写屏障(SATB)着色指针 + 读屏障 + 并发 relocation
分代物理 Region 分 Eden/S/OldJDK 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 StartPause 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 · MetaspaceCompressed 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 调优思路

  1. 明确目标:吞吐量(Parallel)vs 停顿(G1/ZGC)
  2. 减少 Full GC:合理新生代比例;避免大对象打满老年代;排查 Metaspace 类泄漏
  3. 读 GC 日志:关注 停顿时间、频率、回收前后占用、晋升失败、to-space exhausted
  4. 堆 dumpjmap -dump:live + MAT 看 dominator tree、泄漏路径
  5. 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 spaceCCS 上限过小或类过多增大 CompressedClassSpaceSize
CodeCache is fullJIT 编译过多增大 ReservedCodeCacheSize,见 Code Cache
Full GC 频繁老年代满、Metaspace 触发、晋升过快GC 日志、调新生代 / 堆
Minor GC 过频Eden 过小增大新生代或 -Xmx
GC 停顿过长Serial Old、CMS FLS、G1 大堆 Mixed GC换 ZGC(见 ZGC 小节)或 Shenandoah
to-space exhaustedSurvivor 放不下调 SurvivorRatio 或 MaxTenuringThreshold
concurrent mode failureCMS 期间老年代满(历史)已弃用 CMS,换 G1

收集器选型简表

场景推荐
单核 / 小堆客户端Serial
批处理、大吞吐量Parallel GC
通用 Web 服务(JDK 9+)G1
超低延迟、大堆ZGC / Shenandoah
容器内 JavaG1 或 ZGC + 容器感知 -XX:MaxRAMPercentage

相关链接

  • 对象 — Java 对象总览(存活、引用、生命周期入口)
  • JVM — Metaspace、堆布局、ZGC 分代简述
  • 类加载 — 类元数据加载与卸载
  • JIT — Code Cache 与编译触发
  • 启动参数调优 — ZGC / G1 生产参数模板
  • 性能调优 — 应用层优化
  • 内存 — 硬件层 DRAM、Cache 与 JVM 堆的区别