JVM垃圾回收机制深度解析:从算法原理到收集器演进
一、垃圾判定:如何识别"无用对象"
1.1 引用计数法(Reference Counting)
基本原理:
Object A = new Object(); // A的引用计数 = 1
Object B = A; // A的引用计数 = 2
B = null; // A的引用计数 = 1
A = null; // A的引用计数 = 0 → 可回收
致命缺陷:循环引用
class Node {
Node next;
}
Node A = new Node();
Node B = new Node();
A.next = B; // B引用计数 = 2
B.next = A; // A引用计数 = 2
A = null; // A引用计数 = 1
B = null; // B引用计数 = 1
// 循环引用导致内存泄漏!
1.2 可达性分析(Reachability Analysis)
GC Roots对象包括:
- 虚拟机栈中的本地变量
- 静态变量(static成员)
- 常量引用(final static)
- 本地方法栈中的变量
- 活跃线程(Thread对象)
- 类加载器
可达性分析过程:
GC Roots
↓
对象A(可达)
↓
对象B(可达) → 对象C(可达)
↓
对象D(不可达)→ 垃圾对象
二、垃圾回收核心算法
2.1 标记-清除(Mark-Sweep)
执行过程:
- 标记阶段:遍历所有对象,标记存活对象
- 清除阶段:回收未被标记的对象空间
优缺点分析:
- ✅ 实现简单,不需要移动对象
- ❌ 内存碎片:产生不连续内存空间
- ❌ 效率问题:标记和清除都比较耗时
内存变化:
标记前: [使用][空闲][使用][使用][空闲][使用]
标记后: [存活][垃圾][存活][存活][垃圾][存活]
清除后: [存活][空闲][存活][存活][空闲][存活] ← 产生碎片
2.2 复制算法(Copying)
执行过程:
- 将内存分为大小相等的两块(From、To空间)
- 只使用From空间分配对象
- GC时将存活对象复制到To空间
- 清空整个From空间
优缺点分析:
- ✅ 无内存碎片:复制后内存连续
- ✅ 高效:只需遍历存活对象
- ❌ 空间浪费:只能使用一半内存
适用场景: 新生代(对象死亡率高)
2.3 标记-整理(Mark-Compact)
执行过程:
- 标记阶段:同标记-清除算法
- 整理阶段:所有存活对象向一端移动
- 清理边界:清理边界外的内存
优缺点分析:
- ✅ 无内存碎片:内存连续分配
- ✅ 空间利用率高:无需预留备用空间
- ❌ 开销较大:需要移动对象,更新引用
适用场景: 老年代(对象存活率高)
三、分代收集策略
3.1 分代理论基础
对象生命周期特征:
- 新生代:98%的对象"朝生夕死"
- 老年代:长期存活的对象
- 永久代/元空间:类信息、常量等
3.2 分代算法选择
堆内存结构:
新生代 (Young Generation) → 复制算法
├── Eden区 (80%)
├── Survivor0 (10%)
└── Survivor1 (10%)
老年代 (Old Generation) → 标记-整理/标记-清除
对象晋升流程:
- 新对象在Eden区分配
- Minor GC后存活对象进入Survivor区
- 对象年龄计数器+1(每次Minor GC)
- 年龄达到阈值(默认15)晋升老年代
四、垃圾收集器演进历程
4.1 新生代收集器
Serial收集器(JDK 1.3)
- 背景:Java早期,单核CPU为主,应用简单
- 特点:单线程,Stop-The-World
- 算法:复制算法
- 适用:Client模式,内存小的场景
ParNew收集器(JDK 1.4)
- 背景:多核服务器开始普及
- 特点:Serial的多线程版本
- 算法:复制算法
- 适用:多CPU Server环境
Parallel Scavenge收集器(JDK 1.4)
- 背景:企业级应用需要更高吞吐量
- 特点:吞吐量优先
- 算法:复制算法
- 参数:
-XX:MaxGCPauseMillis # 最大GC停顿时间 -XX:GCTimeRatio # 吞吐量大小
吞吐量计算:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
示例:运行100分钟,GC 1分钟 → 吞吐量 = 99/100 = 99%
4.2 老年代收集器
Serial Old收集器(JDK 1.3)
- 背景:与Serial新生代收集器配套
- 特点:Serial的老年代版本
- 算法:标记-整理
Parallel Old收集器(JDK 1.6)
- 背景:配合Parallel Scavenge,形成吞吐量优先组合
- 特点:Parallel Scavenge的老年代版本
- 算法:标记-整理
- 目标:吞吐量优先
4.3 里程碑:CMS收集器(JDK 1.5)
历史背景:
随着2000年后电商和互联网应用的爆发式增长,用户对系统响应时间的要求越来越高。传统的Serial和Parallel收集器在进行Full GC时,停顿时间可能达到数秒,这在高并发的电商场景中是完全不可接受的。CMS应运而生,专门为解决低延迟问题而设计。
四阶段执行过程:
- 初始标记(Stop-The-World):标记GC Roots直接关联对象
- 并发标记:GC线程与用户线程并发执行
- 重新标记(Stop-The-World):修正并发标记期间的变动
- 并发清除:清理垃圾对象
优缺点:
- ✅ 低停顿:大部分工作并发执行,满足电商实时性要求
- ❌ 内存碎片:标记-清除算法导致
- ❌ CPU敏感:并发阶段占用资源
4.4 新一代:G1收集器(JDK 7u4)
历史背景:
随着应用规模不断扩大,堆内存从几GB增长到几十甚至上百GB。CMS在大内存场景下表现不佳:内存碎片问题加剧,并发模式失败风险增加。G1收集器被设计用来替代CMS,在大内存场景下提供更可预测的停顿时间。
革命性改进:
- 区域化:将堆划分为多个Region(1-32MB)
- 预测模型:可设置期望停顿时间
- 混合回收:同时处理新生代和老年代
- 空间整理:在回收过程中进行碎片整理
五、收集器选择策略
5.1 基于应用场景的选择
| 年代 | 场景类型 | 推荐收集器 | 历史背景 |
|---|---|---|---|
| 早期 | 桌面应用/小程序 | Serial + Serial Old | 单核CPU,内存有限 |
| 中期 | 后台计算系统 | Parallel Scavenge + Parallel Old | 批处理任务,追求吞吐量 |
| 电商时代 | Web应用/电商平台 | ParNew + CMS | 高并发,低延迟要求 |
| 现代 | 大数据/云原生 | G1 / ZGC | 大内存,可预测停顿 |
5.2 停顿时间 vs 吞吐量
停顿时间优先(交互式应用):
# 电商时代的选择
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=68
吞吐量优先(批处理应用):
# 计算密集型任务的选择
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:GCTimeRatio=99
现代平衡型(大内存应用):
# JDK 8+ 推荐
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
六、历史演进视角与技术驱动
6.1 技术演进时间线
1995-2000:Serial时代
↓
2000-2005:Parallel时代(服务器多核化)
↓
2005-2012:CMS时代(电商崛起,低延迟需求)
↓
2012-现在:G1时代(大数据,云原生)
↓
未来:ZGC/Shenandoah(亚毫秒停顿)
6.2 业务需求驱动技术变革
电商场景的特定需求:
- 双十一/618:瞬时高并发,GC停顿直接影响交易成功率
- 实时推荐:用户行为分析需要低延迟响应
- 秒杀系统:毫秒级响应要求GC停顿最小化
技术应对策略:
- CMS并发收集:减少STW时间
- G1可预测停顿:满足SLA要求
- ZGC亚毫秒停顿:支撑金融级应用
6.3 收集器演进的核心驱动力
- 硬件发展:单核→多核→NUMA架构
- 应用场景:桌面应用→企业应用→互联网→云原生
- 业务需求:功能性→性能→用户体验→实时性
- 数据规模:MB级→GB级→TB级
七、总结
7.1 核心演进规律
垃圾收集器的演进体现了清晰的业务驱动特征:
- 从正确性到性能:早期保证功能正确,后期优化性能指标
- 从通用性到场景化:一个收集器适应所有场景→不同场景专用收集器
- 从开发者友好到用户友好:减少配置复杂度,提供智能默认值
- 从停顿不可控到可预测:满足现代应用对服务质量的严格要求
7.2 技术选型启示
理解垃圾回收器的历史演进,能够帮助我们在技术选型时:
- 避免技术惯性:不要因为熟悉就一直使用过时的收集器
- 匹配业务特征:根据实际业务场景选择最合适的收集器
- 预见技术趋势:了解技术发展方向,提前做好架构准备
- 平衡技术债务:在稳定性和先进性之间找到平衡点
垃圾回收技术的发展史,本质上是一部应对不断变化的业务需求和技术挑战的创新史。从Serial到G1的演进,不仅反映了Java技术的成熟,更是整个互联网时代技术变革的缩影。