垃圾回收
约 2274 字大约 8 分钟
2025-08-28
垃圾的定义
在 Java 中,垃圾是指 不再被任何活跃的引用所指向的对象,即该对象无法再被程序访问或使用,可以被回收以释放内存空间。
判断对象是否为垃圾的方法
引用计数法(Reference Counting)
- 原理:每个对象维护一个引用计数器,每当有引用指向该对象时计数加 1,引用失效时计数减 1。计数为 0 时判定为垃圾。
- 缺点:无法解决 循环引用 的问题,即两个对象互相引用,但均未被外部引用,仍会占用内存不被回收。
根可达性分析(GC Roots Reachability Analysis)
- 原理:以一系列称为“GC Roots”的对象为起点,从这些对象开始进行图遍历,所有能被访问到的对象都为存活对象,其余未被访问到的对象则为垃圾。
- 常见的 GC Roots 包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(Native 方法)引用的对象
垃圾回收算法
标记 - 清除(Mark-Sweep)
- 过程:
- 标记阶段:标记所有存活对象。
- 清除阶段:清除未被标记的对象。
- 优点:实现简单,效率较高。
- 缺点:
- 产生 内存碎片,可能导致后续分配大对象时无法找到连续空间,引发 Full GC。
- 清除后空闲空间非连续,影响后续性能。
标记 - 整理(Mark-Compact)
- 过程:
- 标记阶段:标记所有存活对象。
- 整理阶段:将存活对象向内存一端移动,然后清理边界以外的空间。
- 优点:避免内存碎片,空间更规整,有利于后续内存分配。
- 缺点:整理阶段需要移动对象,效率低于标记清除算法。
复制(Copying)
- 过程:
- 将内存划分为大小相等的两块区域,每次只使用一块。
- 标记存活对象后,将它们复制到另一块区域,然后清空当前块。
- 优点:
- 无内存碎片。
- 实现高效,适合高回收频率的区域。
- 缺点:内存利用率低,需要两倍的内存空间。
分代垃圾回收机制(Generational Garbage Collection)
Java 堆内存按照对象的生命周期划分为 年轻代(Young Generation) 和 老年代(Old Generation)。
年轻代(Young Generation)
- 对象生命周期:大多数对象是“朝生夕死”。
- GC 类型:Minor GC。
- 常用算法:复制算法(Copying),因为对象存活率低,复制成本小。
- 结构:
- Eden 区
- From Survivor 区
- To Survivor 区
老年代(Old Generation)
- 对象生命周期:对象存活时间较长。
- GC 类型:Major GC 或 Full GC。
- 常用算法:标记 - 整理(Mark-Compact)或标记 - 清除(Mark-Sweep)。
- 标记 - 整理用于避免内存碎片。
- 标记 - 清除用于提高效率,但需配合内存整理策略使用。
垃圾回收器
垃圾回收器(Garbage Collector, GC)是 Java 虚拟机的核心组件,用于自动管理堆内存,回收无用对象以释放空间。下将重点介绍两种面向低延迟场景的垃圾回收器:CMS(Concurrent Mark Sweep)和 G1(Garbage-First)。
| 特性 | CMS | G1 |
|---|---|---|
| 算法类型 | 标记 - 清除 | 标记 - 整理(分区复制) |
| 堆结构 | 分代式(老年代、新生代) | 分区式(Region-based,可动态分配) |
| STW 时间控制 | 不可控,依赖堆大小和对象数量 | 可控,支持设定最大停顿时间目标 |
| 内存碎片问题 | 存在,需额外配置参数应对 | 不存在,回收时整理内存 |
| 浮动垃圾处理 | 存在并发模式失败风险 | 更优,回收策略可动态调整 |
| 适用场景 | 对延迟要求高、堆内存较小(如 4~6GB) | 大堆内存、高吞吐、低延迟、可预测 GC 暂停 |
CMS(Concurrent Mark Sweep)
CMS 垃圾回收器的设计目标是响应时间优先,旨在减少 Stop-The-World (STW) 的发生时间与频率,适用于对低延迟和低停顿敏感的应用系统,如 Web 服务、交易系统等。
CMS 垃圾回收过程
CMS 是一款基于标记 - 清除算法的老年代回收器,其垃圾回收过程分为以下四个阶段:
初始标记(Initial Mark)
- 需要 STW。
- 该阶段仅标记与 GC Roots 直接关联的存活对象,耗时较短。
并发标记(Concurrent Mark)
- 与用户线程并发执行。
- 从初始标记的对象出发,遍历整个对象图,标记所有可达对象。
- 此阶段可以与应用程序同时运行,减少整体停顿。
重新标记(Remark)
- 需要 STW。
- 修复在并发标记阶段由于用户线程运行而导致的引用关系变化,确保最终标记准确。
- 此阶段耗时较长,但通常优于标记 - 整理算法的停顿时间。
并发清理(Concurrent Sweep)
- 与用户线程并发执行。
- 清理未被标记的对象,即垃圾对象,释放内存。
- 此阶段不整理内存空间,仅进行清除。
CMS 如何实现响应时间优先,其 STW 时间为什么很短?
- 并发标记与并发清理阶段:CMS 在这两个阶段允许垃圾回收线程与用户线程同时运行,大幅减少 GC 停顿时间。
- 采用标记 - 清除算法:该算法相较于标记 - 整理算法执行速度更快,避免了内存整理带来的额外开销。
CMS 的主要弊端
内存碎片问题
- 原因:CMS 使用标记 - 清除算法,不进行内存压缩整理,长期运行后老年代会产生大量内存碎片。
- 影响:即使老年代总空间充足,由于碎片化,大对象无法分配,可能触发 Full GC。
浮动垃圾问题(Floating Garbage)
- 定义:在并发清理阶段,用户线程仍可能生成新的垃圾对象。
- 影响:若浮动垃圾过多,导致老年代空间不足,CMS 无法完成并发回收,将触发并发模式失败(Concurrent Mode Failure),继而触发 Full GC。
Full GC 的退化机制
- 当 CMS 无法完成并发回收时,将退化为 Serial Old 垃圾回收器。
- Serial Old 是单线程、STW 的标记 - 整理算法回收器,会导致较长时间的停顿。
G1(Garbage First)
堆内存划分方式
G1 不再采用传统垃圾回收器中“分代”的堆结构(如新生代 + 老年代),而是:
- 将整个堆划分为 多个大小相等的独立区域(Region)。
- 每个 Region 可以是 Eden、Survivor、Old 或 Humongous(用于存储大对象)。
- Region 类型是动态分配的,可以根据需要变化,支持灵活的内存管理策略。
这种“分区(Region-based)”结构,取代了传统的“分代(Generational)”结构,是算法驱动设计的体现。
G1 垃圾回收的详细过程
初始标记(Initial Mark)
- 需要 STW。
- 标记 GC Roots 直接可达的对象,同时触发一次 Young GC。
并发标记(Concurrent Mark)
- 与用户线程并发执行。
- 遍历对象图,标记所有可达对象。
- 还会记录对象的存活信息,用于后续回收效率预测。
最终标记(Final Remark)
- 需要 STW。
- 处理在并发标记期间发生变化的引用关系,完成标记任务。
混合回收(Mixed GC)
- 需要 STW,但可以控制回收哪些 Region。
- G1 根据 Region 中的垃圾比例、回收效率、预期停顿时间等因素,选择部分 Region 进行回收。
- 使用 复制 - 整理算法,将存活对象复制到新的 Region 中,清理旧 Region,避免内存碎片。
实现机制
- 分区管理:堆被划分为多个 Region,每个 Region 可独立回收。
- 回收策略灵活:G1 会优先回收垃圾对象较多的 Region(Garbage First),提高效率。
- 停顿时间可控:通过预测每个 Region 的回收时间,选择合适数量的 Region 进行回收,以满足用户设定的停顿时间目标。
- 复制 - 整理算法:避免了内存碎片问题,使得 G1 更适合长期运行的大内存系统。
GC 调优
GC 调优的目标是根据应用程序的特性选择合适的垃圾回收器,以满足性能或响应时间的要求。
常见调优策略
根据系统需求选择合适的回收器:
- 吞吐量优先:Parallel Scavenge + Parallel Old(默认于 JDK 8)。
- 低延迟需求:CMS(适用于老年代),注意处理内存碎片和并发模式失败。
- 大堆内存 + 可预测停顿:G1,支持高并发、低停顿的全代收集。
设置停顿时间目标:
- G1 中可使用
-XX:MaxGCPauseMillis=200等参数设定期望的最长时间。
- G1 中可使用
调整堆大小与分区大小:
- 增加堆大小或调整 G1 的
RegionSize,以适配内存需求和对象分配模式。
- 增加堆大小或调整 G1 的