JVM 垃圾收集算法解析

一、GC 的核心目标与前置概念

在深入探讨垃圾回收(GC)算法前,我们需要先全面理解GC的本质及其核心目标。GC的本质是自动管理程序运行时内存的生命周期,其主要目标是识别并回收"死亡对象"所占用的内存空间,同时要平衡应用线程的运行稳定性(即尽量减少"Stop The World"问题带来的性能影响)。在实际应用中,GC还需要考虑内存分配的效率、内存碎片的处理以及不同应用场景下的性能优化。

1.1 如何判断对象"已死亡"?

GC过程的第一步也是最关键的一步就是准确区分"存活对象"和"死亡对象"。目前主流JVM(如HotSpot)主要采用两种判断机制:

引用计数法(Reference Counting)

这是一种直观简单的对象存活判断方法。其核心机制是:

  1. 为每个对象维护一个引用计数器
  2. 当对象被引用时,计数器加1(如:Object A = new Object())
  3. 当引用失效时,计数器减1(如:A = null)
  4. 当计数器值为0时,立即标记该对象为可回收状态

优点分析

  • 实现原理简单直接,容易理解
  • 判断过程高效,时间复杂度为O(1)
  • 可以实时回收垃圾对象,不需要等待特定GC周期

缺点分析

  • 无法解决"循环引用"这一经典问题。例如:
    class Node {
        Node next;
    }
    Node a = new Node();
    Node b = new Node();
    a.next = b;
    b.next = a;
    a = b = null; // 虽然a和b都不可达,但引用计数仍为1
    

  • 需要为每个对象维护计数器,增加了存储开销
  • 计数器更新需要原子操作,可能影响多线程性能

实际应用: HotSpot虚拟机未采用该方案,但在Python、Objective-C等语言中广泛使用。在Python中,通过引用计数为主,辅以分代GC来处理循环引用问题。

可达性分析算法(Reachability Analysis)

这是现代JVM(包括HotSpot)采用的核心判断机制。其工作原理是:

  1. 定义一组"GC Roots"作为起始点
  2. 从这些根节点开始,沿着对象引用链向下搜索
  3. 搜索路径称为"引用链"
  4. 任何无法通过这些引用链到达的对象标记为"待回收"

算法优势

  • 彻底解决了循环引用问题
  • 不需要为每个对象维护额外状态
  • 适合大规模堆内存管理

GC Roots的组成(详细说明):

  1. 虚拟机栈中局部变量表引用的对象
    • 包括当前正在运行的方法中的局部变量
    • 参数变量
    • 临时变量等
  2. 方法区中类静态属性引用的对象
    • static修饰的类变量
    • 静态代码块中引用的对象
  3. 方法区中常量引用的对象
    • 字符串常量池中的引用
    • final修饰的常量
  4. 本地方法栈中JNI(Native方法)引用的对象
    • Java Native Interface调用的本地对象
  5. Java虚拟机内部引用
    • 基本数据类型对应的Class对象
    • 常驻异常对象(如NullPointerException)
    • 系统类加载器
  6. 被同步锁持有的对象
    • synchronized关键字持有的对象

实现细节

  • 使用准确式GC(Exact GC)来精确定位引用关系
  • 需要Stop-The-World来保证一致性快照
  • 采用OopMap数据结构加速枚举根节点

1.2 GC的"分代收集"思想

基于对大量实际应用的分析,发现对象生命周期呈现明显的二八分布特征:80%的对象存活时间很短,20%的对象可能长期存活。这种特性催生了分代收集理论。

内存分代模型

现代JVM将堆内存划分为不同代区:

新生代(Young Generation)

  • 特点:
    • 对象存活率极低(统计表明约90%的对象创建后很快死亡)
    • 内存分配频繁
    • 空间相对较小(通常占堆的1/3)
  • 分区:
    • Eden区:新对象分配的主要区域
    • Survivor区(From/To):采用双空间设计,用于保存经历GC后存活的对象
  • GC算法:
    • 采用复制算法(Copying)
    • 过程:Eden存活对象→Survivor,Survivor存活对象年龄+1→另一个Survivor
    • 对象年龄达到阈值(默认15)后晋升老年代

老年代(Old Generation)

  • 特点:
    • 对象存活率高
    • 内存分配相对稳定
    • 空间较大(通常占堆的2/3)
  • GC算法:
    • 标记-清除(Mark-Sweep):简单但会产生内存碎片
    • 标记-整理(Mark-Compact):解决碎片问题但耗时更长
    • 具体选择取决于垃圾收集器实现

永久代/元空间(PermGen/Metaspace)

  • 演变历史:
    • JDK7及之前:永久代存储类信息、常量等
    • JDK8开始:元空间使用本地内存,不再属于堆内存的一部分
  • 回收特点:
    • 主要回收废弃常量和无用的类
    • 触发条件苛刻(Full GC、类加载器回收等)
    • 配置参数:-XX:MetaspaceSize和-XX:MaxMetaspaceSize

分代收集的意义

  1. 针对性优化:不同代区采用最适合的GC算法
  2. 提升效率:大部分GC只需要处理新生代
  3. 降低延迟:Full GC(全堆回收)频率显著减少
  4. 适应对象生命周期特性:短期对象快速回收,长期对象较少处理

典型GC流程示例

  1. 新对象分配在Eden区
  2. Eden区满时触发Minor GC
    • 存活对象复制到Survivor区
    • 年龄递增或晋升老年代
  3. 当老年代空间不足时触发Full GC
    • 对整个堆进行回收
    • 通常伴随应用暂停(Stop-The-World)

二、四大核心 GC 算法深度解析

2.1 复制算法(Copying):新生代的 "高效回收" 方案

2.1.1 原理详解

复制算法是专为新生代设计的回收策略,其核心思想是将内存空间划分为三个区域:

  1. Eden区:新对象首次分配区域,默认占新生代80%空间
  2. Survivor From区:上一次GC后存活对象所在区域
  3. Survivor To区:用于存放本次GC后存活对象的空白区域

具体执行流程如下:

  1. 对象分配阶段

    • 新对象优先在Eden区分配
    • 当Eden区空间不足时,触发Minor GC
    • 大对象(超过Eden区容量)直接进入老年代
  2. 垃圾回收阶段

    • 暂停所有应用线程(STW)
    • 扫描Eden区和From区中的对象
    • 将存活对象按顺序复制到To区
    • 更新对象引用地址
    • 记录对象年龄(每次复制+1)
  3. 空间交换阶段

    • 清空Eden区和From区
    • 交换From区和To区角色(下次GC时当前To区变为From区)

对象晋升机制

  • 当对象年龄达到阈值(默认15)时,从Survivor区晋升到老年代
  • Survivor区空间不足时,部分对象会提前晋升

2.1.2 优缺点深入分析

优势

  1. 高效回收:仅处理存活对象,新生代存活率通常<10%,复制成本低
  2. 无内存碎片:存活对象在To区连续存储,不会产生内存碎片
  3. 简单快速:算法实现简单,适合高频次回收

局限性

  1. 空间浪费:必须保留一半空间用于复制,实际可用内存为90%
  2. 不适合老年代:老年代存活率高,复制大量对象会导致长时间STW
  3. 处理大对象困难:空间划分限制了单个对象的最大尺寸

2.1.3 实战应用与优化

典型应用场景

  • HotSpot VM的Serial GC、ParNew GC、Parallel Scavenge GC的新生代回收
  • 适用于短生命周期对象多的应用场景

性能优化技巧

  1. 调整Survivor比例:通过-XX:SurvivorRatio参数优化空间分配
  2. 控制晋升阈值:-XX:MaxTenuringThreshold调整晋升年龄
  3. 避免过早晋升:确保Survivor区足够容纳多次GC后的存活对象

并发优化案例: ParNew GC通过多线程并行执行复制操作,显著减少STW时间。在8核服务器上,相比单线程的Serial GC,ParNew可以将Minor GC时间缩短70-80%。

2.2 标记 - 清除算法(Mark-Sweep):老年代的 "基础方案"

2.2.1 原理详解

标记-清除算法分为两个阶段执行:

  1. 标记阶段

    • 暂停应用线程(初始标记阶段STW)
    • 从GC Roots开始遍历对象引用关系
    • 使用可达性分析算法标记所有存活对象
    • 在对象头中记录标记状态(通常使用标记位图)
  2. 清除阶段

    • 遍历整个堆内存空间
    • 回收未被标记的对象占用的内存
    • 将释放的内存块加入空闲列表
    • 合并相邻的空闲内存块(部分实现)

并发标记优化: CMS GC通过以下方式减少STW时间:

  1. 初始标记(STW):标记GC Roots直接关联的对象
  2. 并发标记:与用户线程并发执行标记
  3. 重新标记(STW):修正并发标记期间的变化
  4. 并发清除:与用户线程并发回收垃圾

2.2.2 优缺点深入分析

优势

  1. 内存利用率高:无需预留复制空间,全部内存可用
  2. 适合老年代:不移动对象,处理高存活率场景效率较高
  3. STW时间可控:通过并发标记减少暂停时间

局限性

  1. 内存碎片问题

    • 长期运行后产生大量不连续内存块
    • 可能导致"明明有足够内存却无法分配大对象"的情况
    • 最终可能触发Full GC进行碎片整理
  2. 效率问题

    • 需要遍历两次内存(标记和清除)
    • 堆越大,清除阶段耗时越长
    • 并发标记期间可能产生"浮动垃圾"

2.2.3 实战应用与调优

典型应用: CMS GC是老年代使用标记-清除算法的典型代表,适用于:

  • 对延迟敏感的应用(如Web服务)
  • 老年代对象存活周期长的场景

调优参数

  1. -XX:+UseConcMarkSweepGC:启用CMS收集器
  2. -XX:CMSInitiatingOccupancyFraction:设置触发GC的堆占用阈值
  3. -XX:+UseCMSCompactAtFullCollection:Full GC时进行碎片整理

问题排查: 当出现"并发模式失败"时,通常因为:

  1. 老年代空间不足
  2. 并发标记期间应用分配速度过快
  3. 碎片过多导致无法分配大对象

2.3 标记 - 整理算法(Mark-Compact):解决碎片问题的 "优化方案"

2.3.1 原理详解

标记-整理算法在标记-清除基础上增加整理阶段:

  1. 标记阶段

    • 与标记-清除算法相同
    • 通过可达性分析标记所有存活对象
  2. 整理阶段

    • 计算所有存活对象的最终位置
    • 将对象向堆的一端移动(通常向左)
    • 更新所有对象引用指针
    • 执行过程中需要STW
  3. 清除阶段

    • 回收末端的所有空闲空间
    • 更新空闲内存管理数据结构

移动策略差异

  1. 滑动整理:保持对象原始顺序(Serial Old采用)
  2. 线性整理:不考虑对象顺序(Parallel Old采用)

2.3.2 优缺点深入分析

优势

  1. 解决碎片问题

    • 存活对象连续存储
    • 大对象分配成功率提高
    • 减少Full GC触发频率
  2. 内存利用率高

    • 无需预留复制空间
    • 回收后空闲空间集中管理

局限性

  1. STW时间长

    • 对象移动需要暂停所有线程
    • 堆越大,暂停时间越长
    • 不适合对延迟敏感的应用
  2. 实现复杂度高

    • 需要精确更新所有引用
    • 处理跨代引用等特殊情况

2.3.3 实战应用与演进

典型应用

  1. Serial Old GC:单线程标记-整理,适合客户端应用
  2. Parallel Old GC:多线程并行整理,吞吐量优先
  3. G1 GC的Full GC阶段:作为后备方案

优化方向

  1. 并行化

    • Parallel Old使用多线程并行整理
    • 在8核机器上可减少40-60%的STW时间
  2. 区域化整理

    • G1 GC将堆划分为多个Region
    • 优先整理垃圾比例高的Region
  3. 增量整理

    • ZGC采用指针染色技术
    • 实现并发整理,几乎无STW

2.4 分代收集算法(Generational Collection):"组合拳"式的工程方案

2.4.1 原理详解

分代收集的核心思想基于两个经验法则:

  1. 弱代假说:绝大多数对象生命周期很短
  2. 强代假说:经历多次GC仍然存活的对象很可能继续存活

典型内存布局

  1. 新生代(Young Generation)

    • 占堆1/3到1/2空间
    • 采用复制算法
    • 分为Eden和Survivor区
    • 触发Minor GC
  2. 老年代(Old Generation)

    • 占堆大部分空间
    • 采用标记-清除或标记-整理
    • 触发Major/Full GC
  3. 永久代/元空间

    • 存储类元数据等
    • Java 8后改为元空间

跨代引用处理

  • 使用记忆集(Remembered Set)记录跨代引用
  • 避免全堆扫描

2.4.2 为什么分代收集是主流?

技术合理性

  1. 符合对象生命周期分布

    • 90%以上对象在第一次GC时死亡
    • 长寿对象数量少但占用空间大
  2. 优化GC效率

    • 新生代高频回收但速度快
    • 老年代低频回收但彻底
  3. 降低整体延迟

    • 隔离影响,避免每次全堆回收
    • 控制单次GC的STW范围

工程实现优势

  1. 可组合多种算法

    • 新生代用复制算法
    • 老年代根据需求选择不同算法
  2. 可扩展性强

    • G1、ZGC等新型收集器仍保留分代思想
    • 支持渐进式演进
  3. 调优灵活

    • 可独立调整各代大小
    • 针对性优化不同区域

2.4.3 现代分代收集器演进

  1. G1收集器

    • 将堆划分为多个Region
    • 预测各Region回收价值
    • 优先回收垃圾比例高的Region
  2. ZGC收集器

    • 基于分区的并发收集
    • 使用指针染色技术
    • STW时间不超过10ms
  3. Shenandoah

    • 并发整理算法
    • 低延迟优先
    • 适用于大内存场景

选型建议

  1. 吞吐量优先:Parallel Scavenge + Parallel Old
  2. 低延迟优先:ParNew + CMS 或 G1
  3. 超大堆内存:ZGC/Shenandoah

三、HotSpot 虚拟机 GC 算法实战:从回收器到调优

3.1 常见 GC 回收器与算法对应关系

回收器适用区域核心算法线程模型目标场景JDK 支持版本
Serial GC新生代复制算法单线程客户端应用(如桌面程序)JDK 1.3+(默认客户端)
ParNew GC新生代复制算法多线程服务端应用(配合 CMS)JDK 1.4+
Parallel Scavenge新生代复制算法多线程吞吐量优先(后台计算)JDK 1.6+
Serial Old GC老年代标记-整理算法单线程客户端应用(配合 Serial)JDK 1.3+
Parallel Old GC老年代标记-整理算法多线程吞吐量优先(配合 Parallel Scavenge)JDK 1.6+
CMS GC老年代标记-清除算法多线程响应时间优先(Web 应用)JDK 1.5+(JDK 9 弃用)
G1 GC整堆(分 Region)复制+标记-整理多线程平衡吞吐量与响应时间JDK 7+(JDK 9 默认)

3.2 实战案例:CMS GC 的工作流程与调优

CMS(Concurrent Mark Sweep)是 Web 应用中常用的"低延迟"回收器,老年代采用标记-清除算法,核心特点是"并发标记"(与应用线程同时执行),仅在"初始标记"和"重新标记"阶段短暂 STW。

3.2.1 CMS 的 5 个阶段

  1. 初始标记(Initial Mark):STW,标记 GC Roots 直接引用的对象(速度快,STW 时间毫秒级);

    • 示例:标记虚拟机栈中引用的对象、静态变量等。
  2. 并发标记(Concurrent Mark):无 STW,遍历初始标记的对象,标记所有可达的存活对象(耗时久,但与应用线程并行);

    • 示例:遍历对象引用链,标记所有活跃对象。
  3. 并发预清理(Concurrent Preclean):无 STW,处理并发标记期间因应用线程操作产生的"引用变更"(如对象赋值);

    • 示例:处理并发标记期间新增的对象引用。
  4. 重新标记(Remark):STW,再次标记并发标记期间遗漏的存活对象(比初始标记久,但通过"卡表"优化,仍可控);

    • 示例:通过卡表(Card Table)快速定位修改过的内存区域。
  5. 并发清除(Concurrent Sweep):无 STW,回收未标记的死亡对象(与应用线程并行,不影响响应时间)。

    • 示例:清理老年代中未被标记的对象,释放内存空间。

3.2.2 CMS 常见问题与调优参数

问题 1:内存碎片导致 Full GC

  • 调优参数
    • -XX:+UseCMSCompactAtFullCollection:Full GC 后执行整理,减少碎片。
    • -XX:CMSFullGCsBeforeCompaction=5:每 5 次 Full GC 后执行一次整理。
  • 场景:频繁创建和销毁大对象时,容易导致内存碎片。

问题 2:"Concurrent Mode Failure"(并发清除时老年代满)

  • 调优参数
    • -XX:CMSInitiatingOccupancyFraction=75:老年代使用率达 75% 时触发 CMS(默认 92%,提前触发避免满溢)。
    • -XX:+UseCMSInitiatingOccupancyOnly:强制按设定阈值触发。
  • 场景:突发流量导致老年代快速填满,无法完成并发清除。

问题 3:STW 时间过长(重新标记阶段)

  • 调优参数
    • -XX:+ParallelRemarkEnabled:并行执行重新标记,减少 STW 时间。
    • -XX:ConcGCThreads=4:设置并发 GC 线程数(默认 CPU 核心数的 1/4)。
  • 场景:高并发应用重新标记阶段 STW 时间过长,影响用户体验。

3.3 G1 GC:面向大堆的"区域化"回收

G1(Garbage-First)是 JDK 9 后的默认回收器,适用于大堆内存(如 16GB+),核心思想是"将堆划分为多个大小相等的 Region(默认 1MB~32MB)",每个 Region 可动态标记为 Eden、Survivor 或 Old,回收时优先选择"垃圾比例最高的 Region"(Garbage-First),兼顾吞吐量与响应时间。

3.3.1 核心优势

  1. 无全局碎片:回收时通过"复制算法"将存活对象转移到新 Region,避免碎片。
  2. 可预测的 STW 时间:通过 -XX:MaxGCPauseMillis=200(默认 200ms)设置目标停顿时间,G1 会动态调整回收的 Region 数量。
  3. 整堆回收:无需区分新生代和老年代,Region 动态角色切换,适应不同内存场景。

3.3.2 关键调优参数

  • -XX:+UseG1GC:启用 G1 回收器。
  • -XX:G1HeapRegionSize=4m:设置 Region 大小(建议为 2 的幂,范围 1MB~32MB)。
  • -XX:MaxGCPauseMillis=100:设置目标停顿时间(根据业务需求调整,如 Web 应用设 100ms 内)。
  • -XX:InitiatingHeapOccupancyPercent=45:堆内存使用率达 45% 时触发混合回收(Mixed GC,回收 Old Region)。

应用场景

  • 电商大促期间,G1 可以根据设定的 MaxGCPauseMillis 动态调整回收策略,避免因 GC 导致的服务响应延迟。
  • 大数据处理场景中,G1 的整堆回收和 Region 设计能够高效管理大内存对象的生命周期。

四、GC 调优实战:从问题定位到方案落地

4.1 第一步:明确调优目标

GC 调优无 "银弹",需先确定业务优先级:

吞吐量优先场景(后台计算/数据分析)

  • 典型应用:Hadoop MapReduce、Spark批处理、ETL数据处理
  • 特征:允许较长STW(Stop-The-World),通常容忍1-2秒的停顿
  • 优化指标:单位时间内GC总耗时占比<5%
  • 推荐组合:Parallel Scavenge(新生代)+ Parallel Old(老年代)
  • 关键参数
    • -XX:MaxGCPauseMillis=1000(目标最大停顿时间)
    • -XX:GCTimeRatio=19(GC时间与应用时间比1:19)

响应时间优先场景(Web/RPC服务)

  • 典型应用:电商系统、支付系统、API网关
  • 特征:要求单次GC停顿<200ms,99.9%请求延迟<1s
  • 推荐组合
    • JDK8及以下:ParNew(新生代)+ CMS(老年代)
    • JDK9+:G1或ZGC(低延迟GC)
  • 关键参数(以CMS为例):
    • -XX:CMSInitiatingOccupancyFraction=70(老年代70%时触发CMS)
    • -XX:+UseCMSInitiatingOccupancyOnly(固定触发阈值)

4.2 第二步:监控GC状态(关键工具)

jstat命令行工具

  • 基础命令jstat -gcutil <pid> 1000(每1秒输出GC统计)
  • 进阶用法
    # 监控进程12345的GC情况,每5秒输出一次,共输出20次
    jstat -gc -t 12345 5000 20
    
    # 输出各内存区域容量(KB)
    jstat -gccapacity 12345
    

  • 关键指标解读
    • YGC/YGCT:Minor GC次数/总耗时,正常应<50ms/次
    • FGC/FGCT:Full GC次数/总耗时,应尽量避免Full GC
    • O:老年代使用率>75%时需警惕

jvisualvm图形化工具

  • 安装Visual GC插件步骤
    1. 菜单栏选择"工具"→"插件"
    2. 在"可用插件"中勾选"Visual GC"
    3. 点击"安装"并重启
  • 监控功能
    • 实时显示各代内存使用曲线
    • GC事件时间线可视化
    • 对象创建/回收速率统计

GC日志分析

  • 完整日志配置示例
    -Xloggc:/path/to/gc.log
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintHeapAtGC
    -XX:+PrintGCApplicationStoppedTime
    -XX:+PrintPromotionFailure
    -XX:+PrintTenuringDistribution
    

  • 日志分析工具对比
    工具特点适用场景
    GCViewer开源,基础统计图表快速定位明显问题
    GCEasy在线分析,可视化报告团队分享/存档
    HPjmeter企业级,深度诊断复杂性能问题分析

4.3 第三步:常见问题与调优方案

Minor GC频繁优化案例

  • 问题现象
    • Eden区使用率在10分钟内从0%上升到100%
    • YGC频率>5次/分钟
  • 调优步骤
    1. 计算对象分配速率:jstat -gc <pid> 1000 | awk '{print $10}'
    2. 按2倍原则设置Eden区:-Xmn4g -XX:SurvivorRatio=8(Eden=3.5GB)
    3. 观察对象晋升情况:jstat -gcnewcapacity <pid>

Full GC频繁解决方案

  • 内存泄漏排查流程
    1. 获取堆快照:jmap -dump:live,format=b,file=heap.hprof <pid>
    2. 使用MAT工具分析支配树
    3. 重点关注:
      • 重复出现的对象链
      • 占用空间大的集合类
      • 未关闭的资源(如JDBC连接)
  • CMS调优参数
    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=75
    -XX:+UseCMSCompactAtFullCollection
    -XX:CMSFullGCsBeforeCompaction=4
    

G1调优实践

  • Region大小计算
    • 默认RegionSize=堆大小/2048
    • 建议设置:-XX:G1HeapRegionSize=4m(堆>=8GB时)
  • 关键参数组合
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=200
    -XX:InitiatingHeapOccupancyPercent=45
    -XX:ConcGCThreads=4
    -XX:G1ReservePercent=10
    

  • 混合GC调优
    • 观察G1 Mixed GC停顿时间
    • 调整-XX:G1OldCSetRegionThresholdPercent=10

元空间OOM处理

  • 诊断方法
    # 查看元空间使用情况
    jstat -gcmetacapacity <pid>
    
    # 跟踪类加载
    -XX:+TraceClassLoading -XX:+TraceClassUnloading
    

  • 参数建议
    -XX:MetaspaceSize=256m
    -XX:MaxMetaspaceSize=512m
    -XX:+UseCompressedClassPointers
    -XX:+UseCompressedOops
    

  • 常见原因
    • 动态类生成过多(如CGLIB代理)
    • 类加载器泄漏
    • 反射调用频繁

调优黄金法则:每次只修改一个参数,通过A/B测试对比效果,使用-XX:+PrintFlagsFinal验证参数实际生效值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值