3.JVM 垃圾回收与内存管理:Java 程序的「清洁与调度专家」

69 阅读7分钟

一、垃圾回收基础:如何判断 “无用对象”?​

Java 堆中对象占用的内存有限,当对象不再使用时,JVM 的 “清洁工”(GC)会回收内存。而回收前,首先要判断对象是否 “已死亡”,主要靠两种方法:​

  1. 引用计数法:简单的 “计数器”​

每个对象都带一个 “引用计数器”,被引用时加 1,引用失效时减 1。一旦计数器为 0,就代表对象 “无人使用”。但它有个致命缺点 —— 无法解决对象间的 “循环引用” 问题(比如 A 引用 B、B 引用 A,计数器都不为 0,却都无用)。​

  1. 可达性分析法:精准的 “路径搜索”​

从 “GC Roots”(一组必须活跃的引用)开始向下 “探路”,走过的路径叫 “引用链”。若一个对象到 GC Roots 没有任何引用链,就证明它 “已死亡”。​

GC Roots 包含:活跃栈帧中指向堆的对象引用、常量 / 静态变量的引用、已加载类及成员变量的引用等。​

二、对象的 “自救机会”:两次标记的生死考验

一个对象要被真正回收,需经历 “两次标记”,就像 “两次体检”,还有一次 “自救机会”:​

  1. 第一次标记:可达性分析后,无引用链的对象被首次标记,同时判断是否需要执行finalize()方法。​
  • 若未覆盖finalize(),或该方法已执行过,直接判定 “死刑”;​
  • 若需执行,将对象放入 “待处理队列”,由 JVM 线程触发finalize()。​
  1. 第二次标记:执行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。​
  1. YGC 与 Full GC 的触发时机:何时需要 “清洁”​
  • YGC(新生代 GC):E 区分配满时触发,停顿时间短。​
  • Full GC(全堆 GC):老年代空间不足(如大对象直接进入老年代且空间不够)、YGC 后对象无法进入老年代、分配担保失败、元区域空间不足、调用System.gc()(不一定立即执行)等,停顿时间长。​

五、STW 问题与三色标记:减少 “停工时间”​

  1. STW:GC 的 “必要停工”​

执行 GC 时,应用程序其他线程会被 “暂停”(STW,Stop The World),原因有二:​

  • 避免用户线程不断产生新垃圾,导致 GC 永远清不完;​
  • 防止用户线程修改对象引用关系,造成 “漏标”(存活对象被误回收)或 “多标”(无用对象没回收)。​

但 STW 时间过长会影响性能,因此需要优化。​

  1. 三色标记:并发标记的 “高效方案”​

为减少 STW 时间,引入 “三色标记”,将对象分为 3 种状态,实现 “并发标记”(用户线程和 GC 线程同时运行):​

  • 白色:未被标记;​
  • 灰色:已标记,但该对象的引用对象还没标记完;​
  • 黑色:已标记,且所有引用对象都标记完(安全,不会被回收)。​

三色标记流程:​

  1. 初始标记:标记 GC Roots 直接引用的对象为灰色,耗时短(需 STW);​
  1. 并发标记:从灰色对象开始遍历对象图,标记引用对象为灰色、遍历完的为黑色,期间用户线程可正常运行(无需 STW),用 “写屏障” 记录引用修改;​
  1. 重新标记:修正并发标记中被修改的对象状态,耗时短(需 STW);​
  1. 清除:回收白色对象,释放内存。​

通过 “并发标记”,将最耗时的阶段与用户线程并行,大幅缩短 STW 时间。​

六、跨代引用与 Remembered Set:避免 “全堆扫描”​

堆中不同代(如老年代引用新生代对象)存在 “跨代引用”,若每次 GC 都扫描全堆,效率极低。​

“Remembered Set”(记忆集)就像 “跨代引用清单”,记录老年代对象指向新生代对象的引用。GC 时,只需扫描 Remembered Set 中的对象,无需全堆扫描,减少开销,且这些对象会被加入 GC Roots 一起分析。​

七、主流垃圾回收器:不同需求的 “专业团队”​

不同回收器针对不同场景,各有优劣,核心对比如下:​

1. CMS(Concurrent Mark-Sweep):老年代的 “低延迟团队”​

image.png

  • 目标:最短回收停顿时间,工作线程与用户线程并发执行。​
  • 流程:初始标记(STW)→并发标记→预清理→重新标记(STW)→并发清除。​
  • 优点:并发收集、低停顿;​
  • 缺点:占用 CPU 资源、产生内存碎片、无法处理 “浮动垃圾”(并发标记时产生的新垃圾)、可能触发 “Concurrent Mode Failure”(并发清除时老年代满)。​
  • 现状:JDK 14 已废弃。​

2. G1(Garbage-First):平衡吞吐与延迟的 “全能团队”​

image.png

  • 特点:将堆分为 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 级)、低延迟场景​