一、GC 的核心目标与前置概念
在深入探讨垃圾回收(GC)算法前,我们需要先全面理解GC的本质及其核心目标。GC的本质是自动管理程序运行时内存的生命周期,其主要目标是识别并回收"死亡对象"所占用的内存空间,同时要平衡应用线程的运行稳定性(即尽量减少"Stop The World"问题带来的性能影响)。在实际应用中,GC还需要考虑内存分配的效率、内存碎片的处理以及不同应用场景下的性能优化。
1.1 如何判断对象"已死亡"?
GC过程的第一步也是最关键的一步就是准确区分"存活对象"和"死亡对象"。目前主流JVM(如HotSpot)主要采用两种判断机制:
引用计数法(Reference Counting)
这是一种直观简单的对象存活判断方法。其核心机制是:
- 为每个对象维护一个引用计数器
- 当对象被引用时,计数器加1(如:Object A = new Object())
- 当引用失效时,计数器减1(如:A = null)
- 当计数器值为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)采用的核心判断机制。其工作原理是:
- 定义一组"GC Roots"作为起始点
- 从这些根节点开始,沿着对象引用链向下搜索
- 搜索路径称为"引用链"
- 任何无法通过这些引用链到达的对象标记为"待回收"
算法优势:
- 彻底解决了循环引用问题
- 不需要为每个对象维护额外状态
- 适合大规模堆内存管理
GC Roots的组成(详细说明):
- 虚拟机栈中局部变量表引用的对象
- 包括当前正在运行的方法中的局部变量
- 参数变量
- 临时变量等
- 方法区中类静态属性引用的对象
- static修饰的类变量
- 静态代码块中引用的对象
- 方法区中常量引用的对象
- 字符串常量池中的引用
- final修饰的常量
- 本地方法栈中JNI(Native方法)引用的对象
- Java Native Interface调用的本地对象
- Java虚拟机内部引用
- 基本数据类型对应的Class对象
- 常驻异常对象(如NullPointerException)
- 系统类加载器
- 被同步锁持有的对象
- 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
分代收集的意义
- 针对性优化:不同代区采用最适合的GC算法
- 提升效率:大部分GC只需要处理新生代
- 降低延迟:Full GC(全堆回收)频率显著减少
- 适应对象生命周期特性:短期对象快速回收,长期对象较少处理
典型GC流程示例:
- 新对象分配在Eden区
- Eden区满时触发Minor GC
- 存活对象复制到Survivor区
- 年龄递增或晋升老年代
- 当老年代空间不足时触发Full GC
- 对整个堆进行回收
- 通常伴随应用暂停(Stop-The-World)
二、四大核心 GC 算法深度解析
2.1 复制算法(Copying):新生代的 "高效回收" 方案
2.1.1 原理详解
复制算法是专为新生代设计的回收策略,其核心思想是将内存空间划分为三个区域:
- Eden区:新对象首次分配区域,默认占新生代80%空间
- Survivor From区:上一次GC后存活对象所在区域
- Survivor To区:用于存放本次GC后存活对象的空白区域
具体执行流程如下:
-
对象分配阶段:
- 新对象优先在Eden区分配
- 当Eden区空间不足时,触发Minor GC
- 大对象(超过Eden区容量)直接进入老年代
-
垃圾回收阶段:
- 暂停所有应用线程(STW)
- 扫描Eden区和From区中的对象
- 将存活对象按顺序复制到To区
- 更新对象引用地址
- 记录对象年龄(每次复制+1)
-
空间交换阶段:
- 清空Eden区和From区
- 交换From区和To区角色(下次GC时当前To区变为From区)
对象晋升机制:
- 当对象年龄达到阈值(默认15)时,从Survivor区晋升到老年代
- Survivor区空间不足时,部分对象会提前晋升
2.1.2 优缺点深入分析
✅ 优势:
- 高效回收:仅处理存活对象,新生代存活率通常<10%,复制成本低
- 无内存碎片:存活对象在To区连续存储,不会产生内存碎片
- 简单快速:算法实现简单,适合高频次回收
❌ 局限性:
- 空间浪费:必须保留一半空间用于复制,实际可用内存为90%
- 不适合老年代:老年代存活率高,复制大量对象会导致长时间STW
- 处理大对象困难:空间划分限制了单个对象的最大尺寸
2.1.3 实战应用与优化
典型应用场景:
- HotSpot VM的Serial GC、ParNew GC、Parallel Scavenge GC的新生代回收
- 适用于短生命周期对象多的应用场景
性能优化技巧:
- 调整Survivor比例:通过-XX:SurvivorRatio参数优化空间分配
- 控制晋升阈值:-XX:MaxTenuringThreshold调整晋升年龄
- 避免过早晋升:确保Survivor区足够容纳多次GC后的存活对象
并发优化案例: ParNew GC通过多线程并行执行复制操作,显著减少STW时间。在8核服务器上,相比单线程的Serial GC,ParNew可以将Minor GC时间缩短70-80%。
2.2 标记 - 清除算法(Mark-Sweep):老年代的 "基础方案"
2.2.1 原理详解
标记-清除算法分为两个阶段执行:
-
标记阶段:
- 暂停应用线程(初始标记阶段STW)
- 从GC Roots开始遍历对象引用关系
- 使用可达性分析算法标记所有存活对象
- 在对象头中记录标记状态(通常使用标记位图)
-
清除阶段:
- 遍历整个堆内存空间
- 回收未被标记的对象占用的内存
- 将释放的内存块加入空闲列表
- 合并相邻的空闲内存块(部分实现)
并发标记优化: CMS GC通过以下方式减少STW时间:
- 初始标记(STW):标记GC Roots直接关联的对象
- 并发标记:与用户线程并发执行标记
- 重新标记(STW):修正并发标记期间的变化
- 并发清除:与用户线程并发回收垃圾
2.2.2 优缺点深入分析
✅ 优势:
- 内存利用率高:无需预留复制空间,全部内存可用
- 适合老年代:不移动对象,处理高存活率场景效率较高
- STW时间可控:通过并发标记减少暂停时间
❌ 局限性:
-
内存碎片问题:
- 长期运行后产生大量不连续内存块
- 可能导致"明明有足够内存却无法分配大对象"的情况
- 最终可能触发Full GC进行碎片整理
-
效率问题:
- 需要遍历两次内存(标记和清除)
- 堆越大,清除阶段耗时越长
- 并发标记期间可能产生"浮动垃圾"
2.2.3 实战应用与调优
典型应用: CMS GC是老年代使用标记-清除算法的典型代表,适用于:
- 对延迟敏感的应用(如Web服务)
- 老年代对象存活周期长的场景
调优参数:
- -XX:+UseConcMarkSweepGC:启用CMS收集器
- -XX:CMSInitiatingOccupancyFraction:设置触发GC的堆占用阈值
- -XX:+UseCMSCompactAtFullCollection:Full GC时进行碎片整理
问题排查: 当出现"并发模式失败"时,通常因为:
- 老年代空间不足
- 并发标记期间应用分配速度过快
- 碎片过多导致无法分配大对象
2.3 标记 - 整理算法(Mark-Compact):解决碎片问题的 "优化方案"
2.3.1 原理详解
标记-整理算法在标记-清除基础上增加整理阶段:
-
标记阶段:
- 与标记-清除算法相同
- 通过可达性分析标记所有存活对象
-
整理阶段:
- 计算所有存活对象的最终位置
- 将对象向堆的一端移动(通常向左)
- 更新所有对象引用指针
- 执行过程中需要STW
-
清除阶段:
- 回收末端的所有空闲空间
- 更新空闲内存管理数据结构
移动策略差异:
- 滑动整理:保持对象原始顺序(Serial Old采用)
- 线性整理:不考虑对象顺序(Parallel Old采用)
2.3.2 优缺点深入分析
✅ 优势:
-
解决碎片问题:
- 存活对象连续存储
- 大对象分配成功率提高
- 减少Full GC触发频率
-
内存利用率高:
- 无需预留复制空间
- 回收后空闲空间集中管理
❌ 局限性:
-
STW时间长:
- 对象移动需要暂停所有线程
- 堆越大,暂停时间越长
- 不适合对延迟敏感的应用
-
实现复杂度高:
- 需要精确更新所有引用
- 处理跨代引用等特殊情况
2.3.3 实战应用与演进
典型应用:
- Serial Old GC:单线程标记-整理,适合客户端应用
- Parallel Old GC:多线程并行整理,吞吐量优先
- G1 GC的Full GC阶段:作为后备方案
优化方向:
-
并行化:
- Parallel Old使用多线程并行整理
- 在8核机器上可减少40-60%的STW时间
-
区域化整理:
- G1 GC将堆划分为多个Region
- 优先整理垃圾比例高的Region
-
增量整理:
- ZGC采用指针染色技术
- 实现并发整理,几乎无STW
2.4 分代收集算法(Generational Collection):"组合拳"式的工程方案
2.4.1 原理详解
分代收集的核心思想基于两个经验法则:
- 弱代假说:绝大多数对象生命周期很短
- 强代假说:经历多次GC仍然存活的对象很可能继续存活
典型内存布局:
-
新生代(Young Generation):
- 占堆1/3到1/2空间
- 采用复制算法
- 分为Eden和Survivor区
- 触发Minor GC
-
老年代(Old Generation):
- 占堆大部分空间
- 采用标记-清除或标记-整理
- 触发Major/Full GC
-
永久代/元空间:
- 存储类元数据等
- Java 8后改为元空间
跨代引用处理:
- 使用记忆集(Remembered Set)记录跨代引用
- 避免全堆扫描
2.4.2 为什么分代收集是主流?
技术合理性:
-
符合对象生命周期分布:
- 90%以上对象在第一次GC时死亡
- 长寿对象数量少但占用空间大
-
优化GC效率:
- 新生代高频回收但速度快
- 老年代低频回收但彻底
-
降低整体延迟:
- 隔离影响,避免每次全堆回收
- 控制单次GC的STW范围
工程实现优势:
-
可组合多种算法:
- 新生代用复制算法
- 老年代根据需求选择不同算法
-
可扩展性强:
- G1、ZGC等新型收集器仍保留分代思想
- 支持渐进式演进
-
调优灵活:
- 可独立调整各代大小
- 针对性优化不同区域
2.4.3 现代分代收集器演进
-
G1收集器:
- 将堆划分为多个Region
- 预测各Region回收价值
- 优先回收垃圾比例高的Region
-
ZGC收集器:
- 基于分区的并发收集
- 使用指针染色技术
- STW时间不超过10ms
-
Shenandoah:
- 并发整理算法
- 低延迟优先
- 适用于大内存场景
选型建议:
- 吞吐量优先:Parallel Scavenge + Parallel Old
- 低延迟优先:ParNew + CMS 或 G1
- 超大堆内存: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 个阶段
-
初始标记(Initial Mark):STW,标记 GC Roots 直接引用的对象(速度快,STW 时间毫秒级);
- 示例:标记虚拟机栈中引用的对象、静态变量等。
-
并发标记(Concurrent Mark):无 STW,遍历初始标记的对象,标记所有可达的存活对象(耗时久,但与应用线程并行);
- 示例:遍历对象引用链,标记所有活跃对象。
-
并发预清理(Concurrent Preclean):无 STW,处理并发标记期间因应用线程操作产生的"引用变更"(如对象赋值);
- 示例:处理并发标记期间新增的对象引用。
-
重新标记(Remark):STW,再次标记并发标记期间遗漏的存活对象(比初始标记久,但通过"卡表"优化,仍可控);
- 示例:通过卡表(Card Table)快速定位修改过的内存区域。
-
并发清除(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 核心优势
- 无全局碎片:回收时通过"复制算法"将存活对象转移到新 Region,避免碎片。
- 可预测的 STW 时间:通过
-XX:MaxGCPauseMillis=200(默认 200ms)设置目标停顿时间,G1 会动态调整回收的 Region 数量。 - 整堆回收:无需区分新生代和老年代,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 GCO:老年代使用率>75%时需警惕
jvisualvm图形化工具
- 安装Visual GC插件步骤:
- 菜单栏选择"工具"→"插件"
- 在"可用插件"中勾选"Visual GC"
- 点击"安装"并重启
- 监控功能:
- 实时显示各代内存使用曲线
- 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次/分钟
- 调优步骤:
- 计算对象分配速率:
jstat -gc <pid> 1000 | awk '{print $10}' - 按2倍原则设置Eden区:
-Xmn4g -XX:SurvivorRatio=8(Eden=3.5GB) - 观察对象晋升情况:
jstat -gcnewcapacity <pid>
- 计算对象分配速率:
Full GC频繁解决方案
- 内存泄漏排查流程:
- 获取堆快照:
jmap -dump:live,format=b,file=heap.hprof <pid> - 使用MAT工具分析支配树
- 重点关注:
- 重复出现的对象链
- 占用空间大的集合类
- 未关闭的资源(如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验证参数实际生效值。

1052

被折叠的 条评论
为什么被折叠?



