0%

垃圾回收

JVM 垃圾回收(GC):从标记到回收的完整机制

垃圾回收(Garbage Collection,GC)是 JVM 自动管理内存的核心机制,负责识别并回收不再被使用的对象,释放内存空间。GC 主要作用于堆内存方法区,其设计直接影响程序的性能和稳定性。本文将全面解析 GC 的核心原理,包括对象存活判断、回收算法、垃圾回收器及实践配置,帮助深入理解 JVM 内存管理机制。

对象存活判断:如何识别 “垃圾”?

GC 的第一步是判断对象是否存活(即是否为 “垃圾”)。JVM 采用两种核心算法:引用计数法可达性分析

1. 引用计数法(Reference Counting)

  • 原理:为每个对象维护一个 “引用计数器”,当对象被引用时计数器 + 1,引用失效时 - 1。若计数器为 0,则认为对象是垃圾。

  • 优点:实现简单,判断效率高,回收无延迟。

  • 缺点

    • 需额外存储计数器,增加内存开销;
    • 无法解决循环引用问题(如两个对象互相引用,计数器永不为 0,导致无法回收)。

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A { B b; }
    class B { A a; }
    A a = new A();
    B b = new B();
    a.b = b;
    b.a = a;
    a = null;
    b = null;
    // 此时a和b互相引用,计数器均为1,引用计数法无法回收,导致内存泄漏

    由于循环引用问题,Java 未采用此算法。

2. 可达性分析(Reachability Analysis)

  • 原理:以 “根对象集合(GC Roots)” 为起点,通过引用链遍历对象。若对象无法通过任何引用链连接到 GC Roots,则被判定为 “不可达”(垃圾)。
  • 解决循环引用:因 GC Roots 不包含堆内对象的引用,循环引用的对象若与 GC Roots 无连接,会被正确标记为垃圾。
(1)GC Roots 的类型

GC Roots 是不在堆中的引用,常见类型包括:

  • 虚拟机栈中局部变量表的引用(如方法参数、局部变量);
  • 本地方法栈中 JNI(Native 方法)的引用;
  • 方法区中类静态属性的引用(如 static Object obj);
  • 方法区中常量的引用(如字符串常量池的引用);
  • synchronized 持有的对象引用;
  • JVM 内部引用(如基本类型的 Class 对象、系统类加载器)。
(2)对象的三种状态

通过可达性分析,对象会处于以下状态:

  • 可触及的:与 GC Roots 有直接或间接引用链;
  • 可复活的:无引用链,但未执行 finalize() 方法(可能在该方法中重新建立引用链);
  • 不可触及的finalize() 方法已执行且未复活,确定为垃圾。
(3)两次标记过程(最终判定垃圾)

对象需经历两次标记才会被回收:

  1. 第一次标记:对象与 GC Roots 无引用链,进入 “可复活” 状态;
  2. 筛选:判断是否需执行 finalize() 方法(未重写或已执行过则无需执行);
  3. 第二次标记:若需执行 finalize(),对象被放入 F-Queue 队列,由 Finalizer 线程执行该方法;若方法中未重新建立引用链,则被标记为 “不可触及”(垃圾)。

注意:finalize() 仅执行一次,且不建议依赖(执行时机不确定,可能导致对象复活干扰 GC)。

垃圾回收算法:如何回收 “垃圾”?

确定垃圾后,JVM 需通过回收算法释放内存。常见算法包括标记 - 清除复制标记 - 整理,以及基于它们的分代收集分区算法

1. 标记 - 清除算法(Mark-Sweep)

  • 步骤:
    1. 标记:从 GC Roots 遍历,标记所有存活对象;
    2. 清除:遍历堆,回收未标记的垃圾对象,释放内存。
  • 优点:实现简单,无需移动对象。
  • 缺点:
    • 效率低(标记和清除均需全堆遍历);
    • 产生内存碎片(回收后空闲内存不连续,大对象可能无法分配)。

2. 复制算法(Copying)

  • 步骤

    1. 将堆分为两块等大区域(如 From 区和 To 区),仅使用其中一块;
    2. 回收时,将存活对象复制到未使用的区域,清除原区域所有对象;
    3. 交换两块区域的角色(From 变 To,To 变 From)。
  • 优点

    • 效率高(仅复制存活对象);
    • 无内存碎片(复制后内存连续)。
  • 缺点

    • 需双倍内存空间(浪费 50% 内存);
    • 不适合存活对象多的场景(复制成本高)。

    适用场景:新生代(存活对象少,垃圾多)。

3. 标记 - 整理算法(Mark-Compact)

  • 步骤

    1. 标记:同标记 - 清除,标记存活对象;
    2. 整理:将所有存活对象移动到堆的一端,按顺序排列,然后清除边界外的垃圾。
  • 优点

    • 无内存碎片(整理后内存连续);
    • 无需双倍内存(优于复制算法)。
  • 缺点

    • 效率低(标记 + 移动对象,成本高);
    • 移动对象时需调整引用地址,且需 STW(Stop The World)。

    适用场景:老年代(存活对象多,垃圾少)。

4. 分代收集算法(Generational Collection)

分代收集是组合上述算法的复合策略,基于 “对象存活时间不同” 的观察:

  • 新生代:对象存活时间短(如临时变量),采用复制算法(高效处理低存活率对象);

  • 老年代:对象存活时间长(如缓存对象),采用标记 - 清除标记 - 整理算法(避免复制算法的内存浪费)。

    工作流程

    • 新对象优先分配在新生代 Eden 区;
    • Eden 区满时触发 Minor GC(新生代 GC),存活对象移至 Survivor 区;
    • 对象在 Survivor 区经历多次 GC 后,晋升至老年代;
    • 老年代满时触发 Major GC 或 Full GC(老年代 GC)。

5. 分区算法(Region-Based)

将堆划分为多个独立的小区域(Region),每个区域可单独回收。优点是:

  • 控制单次 GC 回收的区域数量,减少 STW 时间;

  • 适合大堆场景(如 10GB 以上),避免全堆扫描。

    代表:G1 收集器(基于分区算法)。

垃圾回收器:算法的具体实现

垃圾回收器是 GC 算法的具体实现,JVM 提供多种回收器,适配不同场景(如低延迟、高吞吐量)。

1. 按线程模式分类

  • 串行回收器:单线程执行 GC,STW 时间长,适合单核环境。
  • 并行回收器:多线程执行 GC,提高吞吐量,STW 时间中等。
  • 并发回收器:GC 线程与用户线程并发执行,降低 STW 时间,适合低延迟场景。

2. 经典垃圾回收器对比

回收器 分代 算法 线程模式 STW 情况 核心特点 适用场景
Serial 新生代 复制 串行 长(单线程) 简单高效,适合小堆、Client 模式 单核环境、简单应用
Serial Old 老年代 标记 - 整理 串行 长(单线程) Serial 老年代版本,作为 CMS 后备 与 Serial/Parallel 配合
ParNew 新生代 复制 并行 中(多线程) Serial 的并行版,可与 CMS 配合 多核环境、需低延迟
Parallel Scavenge 新生代 复制 并行 中(多线程) 吞吐量优先,支持自适应调节 后台任务、批处理(高吞吐量)
Parallel Old 老年代 标记 - 整理 并行 中(多线程) Parallel Scavenge 的老年代版本 与 Parallel Scavenge 配合
CMS 老年代 标记 - 清除 并发 + 并行 短(仅初始 / 重新标记) 低延迟,并发回收 交互式应用(如 Web 服务)
G1 整堆(分区) 复制 + 标记 - 整理 并发 + 并行 可控(按优先级回收) 大堆友好,可预测停顿,无内存碎片 大堆应用(>4GB,如大型服务)

3. 重点回收器详解

(1)CMS 收集器(低延迟优先)

CMS(Concurrent Mark Sweep)是老年代的并发回收器,目标是减少 STW 时间:

  • 步骤:

    1. 初始标记:标记 GC Roots 直接引用的对象(STW,耗时短);
    2. 并发标记:遍历引用链,标记存活对象(与用户线程并发,无 STW);
    3. 重新标记:修正并发标记期间的对象状态变化(STW,耗时短);
    4. 并发清理:回收垃圾对象(与用户线程并发,无 STW)。
  • 优点:并发回收,STW 时间短(低延迟)。

  • 缺点:

    • 产生内存碎片(标记 - 清除算法);
    • 占用 CPU 资源(并发阶段与用户线程竞争);
    • 无法处理 “浮动垃圾”(并发清理时产生的新垃圾,需下次 GC 回收)。
  • 配置参数:

    1
    2
    3
    -XX:+UseConcMarkSweepGC  # 启用 CMS
    -XX:+UseParNewGC # 新生代配合 ParNew(默认开启)
    -XX:CMSInitiatingOccupancyFraction=70 # 老年代占用70%时触发GC
(2)G1 收集器(大堆 + 可预测延迟)

G1(Garbage-First)是面向大堆的分区回收器,兼顾吞吐量和延迟:

  • 核心设计:将堆分为 2048 个 Region(1MB~32MB),每个 Region 可动态标记为 Eden、Survivor 或老年代。

  • 步骤:

    1. 新生代 GC:回收 Eden 和 Survivor 区,存活对象移至其他 Region;
    2. 并发标记:标记全堆存活对象(与用户线程并发);
    3. 混合回收(Mixed GC):优先回收 “价值最高” 的 Region(回收收益 / 时间比最高),兼顾 Eden 和部分老年代。
  • 优点:

    • 无内存碎片(Region 间复制 + 整理);
    • 可预测停顿(通过 -XX:MaxGCPauseMillis 控制);
    • 适合大堆(支持 TB 级内存)。
  • 配置参数:

    1
    2
    3
    -XX:+UseG1GC                # 启用 G1
    -XX:MaxGCPauseMillis=200 # 目标最大停顿时间(默认200ms)
    -XX:G1HeapRegionSize=8m # 每个 Region 大小(1M~32M,2的次幂)

STW 事件(Stop The World)

STW 是 GC 过程中所有用户线程暂停的现象,目的是确保 GC 能在一致的内存快照中进行(避免对象引用动态变化导致标记错误)。

  • 触发场景:可达性分析的根节点枚举、CMS 的初始标记 / 重新标记、G1 的新生代 GC / 混合回收等。
  • 影响:STW 时间过长会导致应用卡顿(如 Web 服务响应延迟),需通过选择合适的回收器和参数优化。

GC 类型与触发条件

根据回收范围,GC 分为部分收集整堆收集

类型 回收范围 触发条件
Minor GC 新生代(Eden + Survivor) Eden 区满时触发
Major GC 老年代 老年代空间不足(仅 CMS 支持单独回收)
Mixed GC 新生代 + 部分老年代 G1 中老年代占用达到阈值(默认 45%)
Full GC 全堆 + 方法区 老年代不足 / 方法区不足 / 显式调用 System.gc()

GC 性能评估与参数配置

1. 核心评估指标

  • 吞吐量:用户代码执行时间 /(用户代码时间 + GC 时间),越高越好(适合后台任务)。
  • 停顿时间:STW 持续时间,越短越好(适合交互式应用)。
  • 内存占用:堆内存使用量,需平衡吞吐量和延迟。

2. 常用 GC 日志参数

通过日志分析 GC 行为,优化参数配置:

1
2
3
4
5
-XX:+PrintGC                # 输出简单 GC 日志
-XX:+PrintGCDetails # 输出详细日志(含区域内存变化)
-XX:+PrintGCDateStamps # 日志带日期时间
-XX:+PrintHeapAtGC # GC 前后打印堆信息
-Xloggc:./gc.log # 日志输出到文件

分析工具:GCViewer、GCEasy(在线工具:gceasy.io)。

3. 回收器选择建议

  • 小堆(<4GB)+ 低延迟:ParNew + CMS
  • 大堆(>4GB)+ 可预测延迟:G1
  • 高吞吐量:Parallel Scavenge + Parallel Old
  • 单核环境:Serial + Serial Old

欢迎关注我的其它发布渠道