JVM GC
# JVM 垃圾回收(GC)深度解析:从原理到实践
在 Java 虚拟机(JVM)的内存管理中,垃圾回收(Garbage Collection,简称 GC)是自动内存管理的核心机制。它负责识别并回收不再被使用的对象,释放内存空间,避免内存泄漏和溢出(OOM)。
# 一、GC 的核心目标与挑战
垃圾回收的核心目标是高效释放不再被使用的内存,但需平衡三个关键指标:
- 准确性:不误删仍在使用的对象(避免空指针异常),不遗漏垃圾对象(避免内存浪费)。
- 效率:回收过程对应用线程的影响(STW 时间)尽可能小。
- 适应性:能适配不同场景(如高吞吐量、低延迟、大堆内存等)。
实现这一目标的核心流程可概括为:如何判定对象已死 → 如何高效回收垃圾 → 如何根据场景选择回收工具。
# 二、对象存活判定:如何识别"垃圾"
要回收垃圾,首先需明确"什么是垃圾"——即不再被任何途径访问的对象。JVM 主要通过两种算法判定对象存活状态:
# 2.1 引用计数法(已淘汰)
- 原理:为每个对象维护一个引用计数器,当对象被引用时计数器+1,引用失效时-1;当计数器为 0 时,判定为垃圾。
- 优势:实现简单,判定效率高。
- 缺陷:无法解决循环引用问题(如 A 引用 B,B 引用 A,两者计数器始终为 1,永远无法被回收)。
- 现状:因循环引用缺陷,JVM 未采用该算法。
# 2.2 可达性分析(JVM 主流算法)
- 原理:以"GC Root"为起点,通过遍历对象引用链(引用关系),若对象无法从任何 GC Root 可达,则标记为垃圾。
- GC Root 范围(核心起点):
- 虚拟机栈(栈帧局部变量表)中引用的对象(如方法参数、局部变量);
- 方法区中类的静态变量引用的对象;
- 本地方法栈中 Native 方法引用的对象;
- 活跃线程对象(如正在运行的线程实例);
- 被同步锁(synchronized)持有的对象。
- 优势:解决了循环引用问题,是 JVM 判定垃圾的标准算法。
# 三、引用类型:影响对象的回收时机
对象的"存活"不仅取决于是否可达,还与引用类型相关。JVM 定义了 4 种引用类型,从强到弱影响对象的回收优先级:
| 引用类型 | 定义与特性 | 回收时机 | 典型场景 |
|---|---|---|---|
| 强引用 | 最常见的引用(如 User user = new User()),默认引用类型。 | 仅当引用链完全断开(如 user = null)才会被回收。 | 普通对象存储(如业务对象)。 |
| 软引用 | 用 SoftReference 包装(如 SoftReference<User> softRef = new SoftReference<>(user))。 | 内存不足时(OOM 前)主动回收。 | 缓存(如图片缓存,内存充足时保留)。 |
| 弱引用 | 用 WeakReference 包装(如 WeakReference<User> weakRef = new WeakReference<>(user))。 | 下次 GC 时必然回收(无论内存是否充足)。 | 临时缓存(如与对象关联的辅助信息)。 |
| 虚引用 | 用 PhantomReference 包装,必须结合引用队列(ReferenceQueue)使用。 | 回收时触发队列通知,对象本身直接被回收。 | 跟踪对象回收时机(如释放堆外内存)。 |
# 四、垃圾收集算法:如何回收垃圾
确定垃圾后,需通过具体算法回收内存。JVM 设计了三种基础算法,各有优劣,适用于不同场景:
# 4.1 复制算法:高效无碎片(空间换时间)
- 原理:将内存划分为大小相等的两块(From 区和 To 区),仅使用 From 区分配对象。GC 时:
- 标记 From 区中所有存活对象;
- 将存活对象完整复制到 To 区(按顺序排列,消除碎片);
- 清空 From 区,交换 From/To 角色(下次使用原 To 区)。
- 优势:
- 回收效率高(仅处理存活对象,无需扫描垃圾);
- 无内存碎片(复制后对象连续排列)。
- 缺陷:
- 空间利用率低(仅 50% 内存可用);
- 存活对象越多,复制成本越高。
- 适用场景:新生代(对象存活率低,复制成本低)。
# 4.2 标记-清除算法:无空间浪费(有碎片)
- 原理:分两个阶段:
- 标记:从 GC Root 出发,标记所有存活对象;
- 清除:扫描整个内存区域,回收所有未标记的垃圾对象。
- 优势:
- 空间利用率高(无需额外空闲区);
- 适合大对象(无需复制,处理成本低)。
- 缺陷:
- 产生内存碎片(回收后空闲内存分散,可能无法分配大对象);
- 效率低(需遍历两次内存:标记一次,清除一次)。
- 适用场景:存活对象少、垃圾多的场景(较少单独使用,多作为基础算法优化)。
# 4.3 标记-整理算法:无碎片(时间换空间)
- 原理:标记-清除算法的优化版,分三个阶段:
- 标记:同标记-清除(标记存活对象);
- 整理:将所有存活对象向内存一端移动并按顺序排列;
- 清除:回收内存另一端的所有垃圾对象。
- 优势:
- 无内存碎片(解决标记-清除的核心缺陷);
- 空间利用率高。
- 缺陷:
- 效率更低(增加了对象移动成本)。
- 适用场景:老年代(对象存活率高,需避免碎片以分配大对象)。
# 五、分代收集:结合算法优势的实战模型
由于对象生命周期差异显著(大部分对象"朝生夕死",少数对象长期存活),JVM 采用分代收集模型,结合上述算法优势:
# 5.1 内存区域划分
JVM 堆内存分为新生代(Young Generation)和老年代(Old Generation),比例通常为 1:2(可通过参数调整)。
| 区域 | 存储对象类型 | 特点 | 收集算法 | 收集触发时机 |
|---|---|---|---|---|
| 新生代 | 新创建的对象(朝生夕死) | 对象存活率低(约 5%) | 复制算法 | Minor GC(轻量 GC) |
| 老年代 | 长期存活的对象(存活久) | 对象存活率高(约 95%) | 标记-整理算法 | Major GC/Full GC |
# 5.2 新生代细节
新生代进一步分为:
- Eden 区(伊甸园):新对象优先分配的区域(占新生代 80%);
- 两个 Survivor 区(From 区和 To 区,各占 10%):用于存放 Minor GC 后存活的对象。
Minor GC 流程:
- 当 Eden 区满时,触发 Minor GC;
- 标记 Eden 区 + From 区的存活对象;
- 将存活对象复制到 To 区,同时对象年龄+1(年龄记录在对象头中);
- 清空 Eden 区和 From 区,交换 From/To 角色;
- 当对象年龄达到阈值(默认 15,可通过
-XX:MaxTenuringThreshold调整),晋升至老年代。
# 5.3 老年代细节
老年代存储长期存活的对象,触发回收的情况包括:
- 老年代内存不足;
- 新生代对象晋升失败(如大对象直接进入老年代,或 Survivor 区对象放不下);
- 显式调用
System.gc()(不推荐,可能触发 Full GC)。
Major GC/Full GC:
- Major GC:仅回收老年代(较少单独触发);
- Full GC:同时回收新生代和老年代(STW 时间长,应尽量避免)。
# 六、垃圾回收器:算法的工程实现
回收器是垃圾收集算法的具体实现,不同回收器针对不同场景(吞吐量、延迟等)优化。主流回收器如下:
# 6.1 Serial 回收器(串行回收)
- 特点:单线程执行 GC,全程触发 STW(Stop The World,暂停所有用户线程)。
- 算法实现:
- 新生代:复制算法;
- 老年代:标记-整理算法。
- 适用场景:单 CPU、小内存(如嵌入式设备)、低并发场景(STW 时间长,不适合高并发)。
- 参数配置:
-XX:+UseSerialGC(启用 Serial + Serial Old 组合)。
# 6.2 Parallel 回收器(并行回收)
- 特点:多线程并行执行 GC,以高吞吐量为目标(吞吐量 = 业务时间 / (业务时间 + GC 时间))。
- 算法实现:
- 新生代:并行复制算法;
- 老年代:并行标记-整理算法。
- 优势:STW 时间比 Serial 短(多线程加速)。
- 适用场景:多 CPU、高吞吐量需求(如后台计算)、对延迟不敏感的场景。
- 参数配置:
-XX:+UseParallelGC(JDK8 默认,新生代 Parallel Scavenge + 老年代 Parallel Old)。
# 6.3 CMS 回收器(并发标记清除,已废弃)
- 特点:以低延迟为目标,老年代回收的大部分阶段与用户线程并发执行(减少 STW 时间)。
- 算法实现:老年代采用标记-清除算法(新生代默认搭配 Parallel 回收器)。
- 回收流程:
- 初始标记:标记 GC Root 直接引用的对象(STW,时间短);
- 并发标记:从初始标记对象出发,遍历所有可达对象(与用户线程并行,无 STW);
- 重新标记:修正并发标记中因用户线程修改引用导致的偏差(STW,时间短);
- 并发清除:回收未标记的垃圾对象(与用户线程并行,无 STW)。
- 缺陷:
- 产生内存碎片(标记-清除算法导致);
- 对 CPU 资源敏感(并发阶段占用 CPU);
- JDK9 标记废弃,JDK14 移除。
- 参数配置:
-XX:+UseConcMarkSweepGC(需搭配-XX:+UseParNewGC启用新生代并行回收)。
# 6.4 G1 回收器(区域化分代式,JDK9+ 默认)
特点:融合分代回收与分区回收,兼顾吞吐量与低延迟,支持大堆内存(数十 GB)。
内存模型:G1 摒弃了 CMS/Parallel Old 的 “连续新生代、连续老年代” 布局,将堆划分为多个大小相等的独立 Region,核心细节如下:
- Region 大小计算:
- Region 大小由 JVM 在启动时自动计算,范围 1MB~32MB(2 的幂次),公式:Region大小 = 堆总大小 / Region数量(默认 Region 数量约 2048 个);
- 可通过参数-XX:G1HeapRegionSize手动指定(需为 2 的幂次,如 1M/2M/4M,不建议随意修改)。
- Region 类型(动态切换):
类型 作用 特殊说明 Eden Region 新生代,存放新创建的对象 多个 Eden Region 可并行回收,占堆比动态调整(无需固定比例,如 ParNew 的 8:1:1) Survivor Region 新生代,存放 Eden 回收后存活的对象 同样动态分配数量,默认分为 S0/S1,但 Region 数量不固定 Old Region 老年代,存放存活时间长的对象 由 Survivor 晋升而来,或大对象拆分后存入 Humongous Region 存放大对象(大小≥Region 一半),占用连续多个 Region 直接划入老年代范畴,回收时需整体处理,避免跨 Region 碎片 - Remembered Set(记忆集,G1 核心优化):
- 问题:Region 是独立的,跨 Region 引用(如 Old Region 引用 Eden Region 对象)会导致 GC 时需要全堆扫描;
- 解决方案:每个 Region 维护一个 Remembered Set,记录 “外部 Region 对本 Region 对象的引用”;
- 实现:通过写屏障(Write Barrier)拦截对象引用更新,异步更新 Remembered Set,避免全堆扫描,大幅降低 GC 耗时。
- Region 大小计算:
核心优势:
- 动态调整:无需固定新老年代比例,根据负载灵活分配 Region 角色;
- 优先回收:每次 GC 优先选择垃圾占比最高的 Region(最大化回收效率);
- 无碎片:采用复制算法(存活对象复制到新 Region)。
回收流程:
- 新生代 GC:
- 暂停所有用户线程(STW);
- 采用复制算法:将 Eden + Survivor 中存活的对象复制到新的 Survivor Region(或晋升到 Old Region);
- 清空原 Eden 和 Survivor Region,标记为空闲;
- 恢复用户线程。
- 混合 GC(同时回收新老年代):
- 初始标记(STW,毫秒级):
- 标记 GC Root 直接引用的对象;
- 借助 Young GC 的 STW 阶段完成,无需额外暂停(优化点)。
- 并发标记(无 STW):
- 从 GC Root 出发,遍历全堆对象图,标记所有存活对象;
- 同时用户线程正常运行,通过写屏障处理 “并发标记期间的对象引用变化”(增量更新)。
- 最终标记(STW,毫秒级):
- 修正并发标记期间因用户线程操作导致的标记偏差;
- 处理 Remembered Set 中的跨 Region 引用,标记遗漏的存活对象。
- 筛选回收(STW,核心优化):
- 统计所有 Region 的垃圾占比(存活对象比例);
- 按 “垃圾占比从高到低” 排序,优先回收垃圾最多的 Region(贪心算法,最大化回收效率);
- 控制本次回收的 Region 数量,确保 STW 时间不超过-XX:MaxGCPauseMillis(默认 200ms);
- 采用复制算法:将存活对象复制到空闲 Region,清空原 Region。
- 新生代 GC:
参数配置:
-XX:+UseG1GC(JDK9+ 默认)。
# 6.5 ZGC 回收器(低延迟,JDK17+ 主流)
特点:专为低延迟、大堆内存(TB 级)设计,STW 时间控制在毫秒级以内。
内存模型:ZGC 继承了 G1 的 “Region 分区” 思想,但做了颠覆性优化 —— 抛弃 G1 固定大小 Region + 静态分配的设计,改为分级 Region + 动态管理,完美适配不同大小对象与弹性内存需求。
- Region 分级设计: ZGC 将 Region 分为三类固定大小的分区,避免 G1 中 “大对象占用连续 Region” 的问题,每个 Region 仅存储对应尺寸的对象:
Region 类型 固定大小 适用对象 核心特点 Small 2MB 小对象(<2MB) 占堆内存 90% 以上,回收效率最高 Medium 32MB 中等对象(2MB~32MB) 避免小 Region 拆分中等对象,减少内存浪费 Large N×2MB 大对象(>32MB) 每个 Large Region 仅存一个大对象,大小为 2MB 整数倍,回收时整体处理 - 动态 Region 管理: 与 G1 启动时一次性创建所有 Region 不同,ZGC 的 Region 具备 “动态弹性”:
- 按需创建:堆内存不足时自动新建 Region,无需启动时预留大量内存;
- 空闲销毁::回收后的空闲 Region 可销毁,释放物理内存(适配容器 / 云环境的内存弹性伸缩);
- 无固定比例:无需像 G1 那样限制新生代 / 老年代占比,完全根据对象分配需求动态调整。
- 抛弃 Remembered Set(RS):G1 为解决跨 Region 引用问题,引入 RS(记忆集)导致额外内存开销(堆的 10%~20%);而 ZGC 通过 “颜色指针” 技术直接解决跨 Region 引用问题,彻底抛弃 RS,额外内存开销仅为堆的 1%~5%,大幅降低资源消耗。
核心优化:ZGC 的灵魂:颜色指针技术
颜色指针是 ZGC 实现 “极致低延迟” 的核心,也是区别于所有传统 GC 的关键 —— 它颠覆了在对象头标记 GC 状态的传统思路,转而通过指针本身标记对象状态。
- 颜色指针的底层原理:
颜色指针的实现依赖硬件架构特性:AMD64(x86_64)架构下,64 位指针仅使用低 48 位(可寻址 256TB 内存),剩余 16 位为 “空闲位”。ZGC 复用这些空闲位作为 “标记位”,直接通过指针标记对象的 GC 状态,无需修改对象头。
简化的指针结构如下:
63~48位:保留位(未使用) 47~0位: 内存地址(实际寻址) └─ 47~44位:颜色标记位(存储对象GC状态)1
2
3- 颜色指针的核心状态:
ZGC 定义了三类核心 “颜色”(状态),通过标记位实现对象状态管理,无需遍历对象:
颜色 状态含义 核心作用 白色 对象未被标记,属于垃圾 回收阶段直接清理 灰色 对象已标记,但子对象未遍历 并发标记阶段的临时状态,需继续遍历子对象 黑色 对象及所有子对象均已标记 存活对象,回收阶段保留 转发色(扩展) 对象已复制到新 Region 重分配阶段临时状态,指引用户线程访问新对象地址 - 颜色指针的核心优势:
- 无全局扫描:通过指针直接判断对象状态,无需遍历全堆 / Region,大幅减少 GC 耗时;
- 轻量级并发:用户线程访问对象时,CPU 的内存保护机制(MMU)自动重定向到最新对象地址,无需复杂写屏障;
- STW 极短:所有状态标记在指针层面完成,仅初始 / 最终标记需极短 STW,无修改对象头的额外开销。
回收流程:
- 初始标记(STW,微秒级):
- 核心操作:标记 GC Root 直接引用的对象(如虚拟机栈、本地方法栈引用);
- 耗时特点:仅与 GC Root 数量相关,与堆大小无关,通常 < 1ms。
- 并发标记(无 STW):
- 核心操作:从 GC Root 出发,遍历对象图,通过颜色指针标记所有存活对象;
- 关键优化:用户线程正常运行,ZGC 通过 “读屏障” 处理并发标记期间的对象引用变化,无性能瓶颈。
- 并发预备重分配:
- 核心操作:统计所有 Region 的垃圾占比,按 “垃圾占比从高到低” 筛选待回收 Region(贪心策略);
- 前置准备:为待回收 Region 分配新的空闲 Region,准备复制存活对象。
- 重分配标记(STW,微秒级):
- 核心操作:修正并发标记期间因用户线程操作导致的指针引用偏差;
- 关键目标:标记所有指向 “待回收 Region” 的 GC Root 引用,耗时同样 < 1ms。
- 并发重分配(核心阶段,无 STW):
- 核心操作:将待回收 Region 中的存活对象复制到新 Region;
- 透明转发:用户线程访问旧 Region 对象时,通过 “转发色指针” 自动重定向到新对象地址,无感知。
- 并发重映射(无 STW):
- 核心操作:异步更新所有指向旧 Region 的指针,使其直接指向新对象地址;
- 容错设计:即使该阶段未完成,读屏障也会兜底转发,不影响业务运行。
核心结论:ZGC 的 STW 时间仅来自 “初始标记 + 重分配标记”,且总耗时不随堆大小增长 ——1GB 堆和 1TB 堆的 STW 时间几乎一致。
- 初始标记(STW,微秒级):
适用场景:对延迟敏感的大型应用(如分布式服务、大数据处理)。
参数配置:
-XX:+UseZGC(JDK11 引入,JDK17 成为长期支持版本)。
# 七、总结:如何选择合适的 GC 策略
GC 调优的核心是匹配业务场景:
- 高吞吐量优先:选择 Parallel 回收器(如后台批处理任务);
- 低延迟优先:选择 G1 或 ZGC(如 Web 服务、实时交易系统);
- 小内存/嵌入式:选择 Serial 回收器。