一、垃圾回收基础:如何判断 “无用对象”?
Java 堆中对象占用的内存有限,当对象不再使用时,JVM 的 “清洁工”(GC)会回收内存。而回收前,首先要判断对象是否 “已死亡”,主要靠两种方法:
- 引用计数法:简单的 “计数器”
每个对象都带一个 “引用计数器”,被引用时加 1,引用失效时减 1。一旦计数器为 0,就代表对象 “无人使用”。但它有个致命缺点 —— 无法解决对象间的 “循环引用” 问题(比如 A 引用 B、B 引用 A,计数器都不为 0,却都无用)。
- 可达性分析法:精准的 “路径搜索”
从 “GC Roots”(一组必须活跃的引用)开始向下 “探路”,走过的路径叫 “引用链”。若一个对象到 GC Roots 没有任何引用链,就证明它 “已死亡”。
GC Roots 包含:活跃栈帧中指向堆的对象引用、常量 / 静态变量的引用、已加载类及成员变量的引用等。
二、对象的 “自救机会”:两次标记的生死考验
一个对象要被真正回收,需经历 “两次标记”,就像 “两次体检”,还有一次 “自救机会”:
- 第一次标记:可达性分析后,无引用链的对象被首次标记,同时判断是否需要执行finalize()方法。
- 若未覆盖finalize(),或该方法已执行过,直接判定 “死刑”;
- 若需执行,将对象放入 “待处理队列”,由 JVM 线程触发finalize()。
- 第二次标记:执行finalize()时,若对象能在方法中重新与引用链建立关联(比如被其他对象引用),就会 “自救成功”,脱离回收队列;若没关联,则二次标记后被回收。
注意:每个对象的finalize()只能执行一次,错过就没机会了。
三、方法区的回收:不止堆内存需要 “清洁”
方法区(元区域)也需回收,主要针对 “废弃常量” 和 “无用的类”:
- 废弃常量:若常量池中的字面量(如字符串)没有任何对象引用,也无其他地方使用,就会被回收。
- 无用的类:需同时满足 3 个条件:堆中无该类的任何实例、加载该类的类加载器已回收、该类的 Class 对象无任何引用(无法通过反射访问)。
四、内存分配与回收策略:不同区域的 “管理方案”
1. 垃圾收集算法
针对性 “清洁工具”不同内存区域的对象特性不同,对应不同的 “清洁工具”:
| 算法 | 核心逻辑 | 适用场景 | 优缺点 |
|---|---|---|---|
| 标记 - 清除 | 先标记无用对象,再统一回收 | 老年代(对象存活率高) | 效率低、产生内存碎片 |
| 复制算法 | 将内存分为两块,只用一块;回收时将存活对象复制到另一块,再清空原块 | 新生代(对象存活率低) | 无碎片、效率高,浪费一半内存 |
| 标记 - 整理 | 标记无用对象后,将存活对象向一端 “靠拢”,再清理边界外内存 | 老年代 | 无碎片,需移动对象、效率略低 |
| 分代算法 | 新生代用复制算法、老年代用标记 - 清除 / 标记 - 整理,按需选择 | 全堆 | 兼顾效率与无碎片 |
2. 新生代的 “分配担保”:老年代的 “兜底作用”
新生代采用 “复制算法”,分为 1 块 “伊甸区”(E 区)和 2 块 “幸存者区”(S 区),每次只用 E 区和 1 块 S 区。回收时,将存活对象复制到另一块 S 区,清空原区域。
若 S 区空间不足,存活对象会 “求助” 老年代 —— 这就是 “分配担保”,流程如下:
- YGC(新生代 GC)前,先检查老年代最大可用连续空间是否大于新生代对象总大小:
- 大于:YGC 安全;
- 小于:看 “历次晋升老年代的平均大小”,若大于则尝试 YGC(有风险),否则直接触发 Full GC(全堆 GC)。
- YGC 后:存活对象小于 S 区则进 S 区,大于 S 区但小于老年代空间则进老年代,若都放不下则触发 Full GC。
- YGC 与 Full GC 的触发时机:何时需要 “清洁”
- YGC(新生代 GC):E 区分配满时触发,停顿时间短。
- Full GC(全堆 GC):老年代空间不足(如大对象直接进入老年代且空间不够)、YGC 后对象无法进入老年代、分配担保失败、元区域空间不足、调用System.gc()(不一定立即执行)等,停顿时间长。
五、STW 问题与三色标记:减少 “停工时间”
- STW:GC 的 “必要停工”
执行 GC 时,应用程序其他线程会被 “暂停”(STW,Stop The World),原因有二:
- 避免用户线程不断产生新垃圾,导致 GC 永远清不完;
- 防止用户线程修改对象引用关系,造成 “漏标”(存活对象被误回收)或 “多标”(无用对象没回收)。
但 STW 时间过长会影响性能,因此需要优化。
- 三色标记:并发标记的 “高效方案”
为减少 STW 时间,引入 “三色标记”,将对象分为 3 种状态,实现 “并发标记”(用户线程和 GC 线程同时运行):
- 白色:未被标记;
- 灰色:已标记,但该对象的引用对象还没标记完;
- 黑色:已标记,且所有引用对象都标记完(安全,不会被回收)。
三色标记流程:
- 初始标记:标记 GC Roots 直接引用的对象为灰色,耗时短(需 STW);
- 并发标记:从灰色对象开始遍历对象图,标记引用对象为灰色、遍历完的为黑色,期间用户线程可正常运行(无需 STW),用 “写屏障” 记录引用修改;
- 重新标记:修正并发标记中被修改的对象状态,耗时短(需 STW);
- 清除:回收白色对象,释放内存。
通过 “并发标记”,将最耗时的阶段与用户线程并行,大幅缩短 STW 时间。
六、跨代引用与 Remembered Set:避免 “全堆扫描”
堆中不同代(如老年代引用新生代对象)存在 “跨代引用”,若每次 GC 都扫描全堆,效率极低。
“Remembered Set”(记忆集)就像 “跨代引用清单”,记录老年代对象指向新生代对象的引用。GC 时,只需扫描 Remembered Set 中的对象,无需全堆扫描,减少开销,且这些对象会被加入 GC Roots 一起分析。
七、主流垃圾回收器:不同需求的 “专业团队”
不同回收器针对不同场景,各有优劣,核心对比如下:
1. CMS(Concurrent Mark-Sweep):老年代的 “低延迟团队”
- 目标:最短回收停顿时间,工作线程与用户线程并发执行。
- 流程:初始标记(STW)→并发标记→预清理→重新标记(STW)→并发清除。
- 优点:并发收集、低停顿;
- 缺点:占用 CPU 资源、产生内存碎片、无法处理 “浮动垃圾”(并发标记时产生的新垃圾)、可能触发 “Concurrent Mode Failure”(并发清除时老年代满)。
- 现状:JDK 14 已废弃。
2. G1(Garbage-First):平衡吞吐与延迟的 “全能团队”
- 特点:将堆分为 2048 个大小相等的 “Region”,保留新生代 / 老年代概念但无物理隔离;采用标记 - 整理算法,无内存碎片;支持 “可预测停顿时间”(按用户设置的预期停顿时间制定回收计划)。
- 流程:初始标记(STW)→并发标记→最终标记(STW)→筛选回收(按 Region 的回收价值排序,优先回收收益高的,可并发执行)。
- 适用场景:JDK 9 + 默认回收器,适合大堆内存(如几 GB 到几十 GB)。
3. ZGC(Z Garbage Collector):超低延迟的 “高端团队”
- 核心亮点:毫秒级 STW(通常 < 10ms)、支持 TB 级堆内存、全并发回收(标记 / 转移 / 压缩均与用户线程并发)、动态压缩无碎片、JDK 21 + 支持分代优化。
- 适用场景:金融交易、实时系统等对低延迟要求极高的场景。
- 缺点:CPU 占用略高于 G1,但为低延迟牺牲是可接受的。
三者对比总结
| 回收器 | 设计目标 | 回收方式 | 内存管理 | 适用场景 |
|---|---|---|---|---|
| CMS | 低延迟 | 并发标记 - 清除(老年代) | 有碎片 | 中小堆、容忍偶尔 Full GC |
| G1 | 平衡吞吐与延迟 | 分代 + 分区回收 | 增量压缩无碎片 | 通用场景、大堆(GB 级) |
| ZGC | 超低延迟 | 全并发回收 | 动态压缩无碎片 | 超大堆(TB 级)、低延迟场景 |