JVM垃圾回收器

一、基础核心概念

1.1 JVM内存模型

JVM 运行时数据区划分两大板块:线程私有区域、线程共享区域;其中只有堆和方法区会发生GC,其余区域线程消亡自动释放。

线程私有区域

  • 程序计数器:当前线程执行的字节码行号,无GC、无OOM,JVM唯一无内存溢出的区域。

  • 虚拟机栈:方法执行的数据结构,存储栈帧;栈帧包含局部变量表、操作数栈、动态链接、方法返回地址;线程退出后栈内存自动释放。

  • 本地方法栈:结构与虚拟机栈一致,专门服务于Native本地方法。

线程共享区域

  • 堆(Heap):所有对象实例、数组分配区域,GC主战场;元空间不属于堆内存

    • 分代模型:新生代(Eden、2块Survivor)、老年代。

    • 对象分配规则:优先Eden分配、TLAB线程私有缓冲区分配、逃逸分析栈上分配、大对象直接进入老年代、长期存活对象晋升老年代。

  • 方法区(Method Area):存储类元信息、运行时常量池、静态变量、即时编译代码。

    • 元空间(Metaspace):JDK8 彻底替代永久代,使用操作系统本地内存,默认无内存上限,解决永久代OOM问题;可通过 -XX:MaxMetaspaceSize 手动限制大小。

对象生命周期

  • 分配:new 对象优先存入Eden区,超大对象直接进入老年代。

  • 存活:通过可达性分析判断,GC Roots可达则判定存活。

  • 回收:不可达对象,经标记、清理、压缩后释放内存。

1.2 可达性分析与GC Roots

主流JVM采用可达性分析算法判定对象存活,摒弃传统引用计数法(解决循环引用无法回收问题)。

GC Roots存活根对象(面试必背)

  • 虚拟机栈:栈帧中局部变量表引用的对象。

  • 方法区:静态变量、常量池引用的对象。

  • 本地方法栈:JNI本地方法引用的对象。

  • 同步锁:Synchronized 持有的锁对象。

  • 虚拟机内部:Class对象、异常对象、系统内置引用。

四种引用类型(强度从高到低)

  • 强引用Object obj = new Object();,只要引用存在,永不回收,默认引用。

  • 软引用(SoftReference):内存充足不回收,内存溢出前强制回收,适合做缓存。

  • 弱引用(WeakReference):只要发生GC,无论内存是否充足都会回收,典型应用:ThreadLocalMap。

  • 虚引用(PhantomReference):无引用能力,仅用于跟踪对象回收状态,配合引用队列使用。

二、垃圾回收算法

2.1 基础算法

标记-清除(Mark-Sweep)

  • 流程:遍历标记存活对象,统一清除未标记垃圾对象。

  • 优点:算法简单、无需移动对象。

  • 缺点:产生大量内存碎片,分配大对象容易OOM;扫描效率低。

标记-整理(Mark-Compact)

  • 流程:标记存活对象,将存活对象向内存一端平移压缩,清空边界以外垃圾。

  • 优点:无内存碎片,内存空间规整。

  • 缺点:移动对象成本高、效率低,适合存活率高的老年代。

复制(Copying)

  • 流程:将内存划分为两块同等大小区域,只使用其中一块;GC时将存活对象复制到空闲区域,清空原区域。

  • 优点:无碎片、复制效率高,无需复杂标记计算。

  • 缺点:内存利用率仅有50%,浪费空间;适合存活率极低的新生代。

2.2 分代收集理论

依据对象存活周期差异化回收,不同年代适配不同算法,是现代GC核心思想。

  • 新生代(Young Gen):对象朝生夕死、存活率极低,采用复制算法;代表收集器:Parallel Scavenge、G1。

  • 老年代(Old Gen):对象存活时间长、存活率高,采用标记-清除/标记-整理;代表收集器:Parallel Old、CMS、G1。

  • 元空间:存储类元数据,回收频率极低,仅卸载无用类、释放无用类加载器。

补充:对象晋升规则

  • 年龄晋升:默认存活15次MinorGC晋升老年代(对象头4bit存储分代年龄,最大值15)。

  • 动态年龄判断:Survivor区相同年龄对象总和超过50%,大于等于该年龄对象直接晋升。

三、主流垃圾收集器

3.1 G1垃圾收集器(Garbage-First)

JDK9 默认收集器,面向大内存、低延迟设计,兼顾吞吐量与停顿时间。

核心特性

  • 分区(Region):将堆划分为固定大小Region(1MB~32MB),逻辑分代、物理不分代;包含Eden、Survivor、Old、Humongous巨型分区。

  • 可预测停顿:通过 -XX:MaxGCPauseMillis 自定义最大停顿时间,可控STW。

  • Remembered Set(RS):基于卡表(Card Table)实现,记录跨代引用,避免MinorGC全局扫描老年代。

  • 并发标记:业务线程与GC线程并发执行,减少STW耗时。

垃圾回收流程

G1 只有两类GC:YoungGC、MixedGC;并发标记为MixedGC前置准备阶段。

  1. Young GC(STW)

    • 根扫描:快速扫描GC Roots;

    • 更新RS:处理脏卡队列,刷新跨代引用;

    • 对象复制:存活对象转移至Survivor区,达标对象晋升老年代;

    • 清空Eden:回收全部Eden分区内存。

  2. 并发标记周期(MixedGC前置)

    • 初始标记(STW):标记GC Roots直接可达对象,耗时极短;

    • 根区域扫描(并发):扫描Survivor区引用的老年代对象;

    • 并发标记:GC线程遍历对象图,标记全部存活对象;

    • 重新标记(STW):修正并发期间引用变更,采用SATB算法;

    • 筛选清理(STW):统计Region垃圾占比,筛选高垃圾分区。

  3. Mixed GC(STW)

    • 分区筛选:根据停顿时间目标,选取新生代+垃圾最多的老年代Region;

    • 复制回收:迁移存活对象,批量释放选中分区内存。

关键机制

  • SATB(快照起始算法):标记开始瞬间保存对象引用快照,解决并发标记过程中对象消失导致的漏标问题。

  • 巨型对象(Humongous):占用空间超过1/2 Region的对象,单独分配专属分区,避免复制开销;默认不回收,MixedGC统一清理。

  • 卡表(Card Table):堆内存划分512B卡片,记录引用变更,是Remembered Set的底层实现。

3.2 ZGC垃圾收集器(Z Garbage Collector)

JDK11实验特性、JDK15正式发布、JDK21默认开启分代ZGC;面向超大堆、亚毫秒级低延迟设计。

核心特性

  • 亚毫秒级停顿:停顿时间不随堆内存增大而变长,标准停顿时间 <10ms。

  • 染色指针(Colored Pointers):复用64位指针高位,存储标记、重定位状态,无需额外内存存储元数据。

  • 读屏障(Load Barrier):访问对象时触发拦截,处理引用修正、对象迁移,无写屏障开销。

  • 逻辑不分代:JDK21前无物理分代,JDK21+分代优化,针对性回收短期存活小对象。

垃圾回收流程

  1. 标记阶段

    • 初始标记(STW):仅标记GC Roots直接引用对象,耗时与堆大小无关;

    • 并发标记:遍历对象图,读屏障拦截并发修改,标记存活对象;

    • 再标记(STW):短暂修正并发期间引用变化,耗时通常<1ms。

  2. 重定位阶段

    • 并发准备重定位:筛选垃圾率高的内存页,组成重分配集;

    • 初始重定位(STW):优先迁移GC Roots引用对象;

    • 并发重定位(核心):GC线程+业务线程协同迁移对象;业务线程访问旧对象时,读屏障自动迁移对象、修正指针,实现自愈;

    • 并发清理:对象迁移完成,原内存页立即释放归还系统。

  3. 延迟重映射:遗留旧指针不立即修正,延迟至下一轮GC并发标记阶段处理,降低单次停顿压力。

关键机制

  • 染色指针状态
指针高位 含义
00 Marked0
01 Marked1
10 Remapped
  • 读屏障伪代码
1
2
3
4
5
// 访问对象时自动触发读屏障
Object obj = field; 
if (isMarked(obj)) {
  obj = relocate(obj); // 迁移对象并重定向指针
}

四、性能调优与故障排查

4.1 调优目标

  • 低延迟:ZGC、Shenandoah、G1(MixedGC),适用于互联网高并发业务。

  • 高吞吐量:Parallel GC,适用于离线批处理、大数据计算。

  • 低内存占用:合理设置堆区间、规避内存泄漏、优化对象存活率。

4.2 关键参数

G1 核心参数

1
2
3
-XX:MaxGCPauseMillis=200ms # 最大目标停顿时间
-XX:G1HeapRegionSize=4M # 单个Region分区大小
-XX:InitiatingHeapOccupancyPercent=45 # 老年代占用45%触发并发标记

ZGC 核心参数

1
2
3
-XX:+UnlockExperimentalVMOptions # 低版本启用ZGC
-XX:MaxGCPauseMillis=10ms # 亚毫秒级停顿目标
-XX:+UseZGC # 显式启用ZGC

4.3 故障排查工具

  • jstat -gcutil &lt;pid&gt;:实时查看GC次数、内存占用、GC耗时。

  • jmap -dump:live,format=b,file=heap.bin &lt;pid&gt;:生成堆转储快照,分析内存对象。

  • jstack &lt;pid&gt;:导出线程栈,排查死锁、线程阻塞。

  • -Xlog:gc*:打印详细GC日志,示例:-Xlog:gc*:file=gc.log:utctime,level=debug

  • 可视化工具:JVisualVM、MAT(堆内存分析)、GCEasy(GC日志分析)。

4.4 常见问题与解决方案

Full GC频繁(高频考点)

  • 老年代空间不足:调大堆内存、优化大对象生成逻辑。

  • 元空间溢出:调整 -XX:MaxMetaspaceSize,排查动态生成类。

  • 直接内存泄漏:限制 -XX:MaxDirectMemorySize,排查NIO堆外内存。

  • 额外诱因:晋升失败、空间担保失败、手动调用 System.gc()

CPU使用率高

  • G1混合GC过多:适当调大停顿时间,减少MixedGC触发频率。

  • ZGC重定位压力大:排查高频创建短期大对象,优化业务代码。

内存泄漏

通过MAT分析堆转储文件,定位大对象、静态集合常驻对象、循环引用对象。

五、前沿技术与未来趋势

  • Shenandoah:低延迟收集器,采用Brooks Pointer转发指针实现并发压缩,停顿时间与堆大小无关;劣势:吞吐量偏低。

  • Epsilon GC:无操作GC,不执行垃圾回收;用于性能测试、短生命周期微任务。

  • 分代ZGC:JDK21正式默认,区分新生代、老年代,优化小对象回收效率,降低GC开销。

  • Loom项目:虚拟线程,轻量化用户线程,降低线程创建与切换开销,优化GC线程调度模型。

六、最佳实践与总结

收集器选择标准(企业实战选型)

业务场景 推荐收集器 选型理由
堆 < 4GB、离线任务 Parallel GC 高吞吐量、实现简单、资源占用低
堆 4GB~32GB、通用业务 G1 平衡延迟与吞吐量,可控停顿时间
堆 > 32GB、高并发低延迟 ZGC / Shenandoah 亚毫秒级停顿,适配超大堆、实时业务

调优原则

  1. 先监控,后调优:基于GC日志、监控指标定位问题,禁止盲目改参数。

  2. 避免过早优化:优先优化业务代码、减少无效对象创建,再调整JVM参数。

  3. 关注核心指标:吞吐量、STW延迟、内存使用率、GC触发频率。

架构师决策点

  • 业务评估:区分实时联机业务(低延迟)、离线批处理业务(高吞吐)。

  • 容量规划:预估业务峰值,预留堆内存冗余,避免内存溢出。

  • 持续监控:接入APM工具(Prometheus+Grafana),实时监控GC指标,动态调优。