1. JVM垃圾收集机制概述
在Java开发中,垃圾收集(GC)是JVM自动管理内存的核心机制。作为Java程序员,我们可能很少直接操作内存分配与回收,但理解GC原理对写出高性能应用至关重要。我经历过不少因为GC问题导致的性能瓶颈,比如某次线上服务突然出现长时间卡顿,最终定位到是Full GC频繁触发导致的。
JVM的垃圾收集器负责自动回收不再使用的对象,释放堆内存空间。这个过程看似简单,实则涉及复杂的算法和策略选择。不同的垃圾收集器采用不同的算法,适用于不同的应用场景。比如我们常用的CMS、G1、ZGC等收集器,各有其设计哲学和适用场景。
提示:虽然现代JVM的GC已经相当智能,但不合理的对象创建和引用管理仍然会导致严重的GC问题。理解GC原理能帮助我们写出更"GC友好"的代码。
2. 垃圾收集基础理论
2.1 对象存活性判断
垃圾收集的首要问题是判断哪些对象是"垃圾"。JVM主要使用两种算法:
引用计数法:每个对象维护一个引用计数器,当引用数为0时即可回收。这种方法简单但无法解决循环引用问题。比如对象A引用B,B又引用A,即使外部不再引用它们,计数器也不为0。
可达性分析算法:这是JVM实际采用的方案。从GC Roots(如虚拟机栈引用的对象、方法区静态属性引用的对象等)出发,遍历对象引用链,不可达的对象即为可回收的。
// 循环引用示例 class Node { Node next; } public class Main { public static void main(String[] args) { Node a = new Node(); Node b = new Node(); a.next = b; b.next = a; // 形成循环引用 a = null; b = null; // 虽然a和b置为null,但引用计数不为0 } }2.2 分代收集理论
基于"弱分代假说"(绝大多数对象朝生夕死)和"强分代假说"(熬过越多次GC的对象越难消亡),JVM将堆内存划分为不同代:
| 代区 | 特点 | 典型回收频率 | 收集算法 |
|---|---|---|---|
| 新生代 | 对象生命周期短 | 高 | 复制算法 |
| 老年代 | 对象生命周期长 | 低 | 标记-清除/整理 |
| 永久代/元空间 | 类元数据等 | 极低 | - |
这种分代设计使得JVM可以针对不同特性的对象采用不同的收集策略,提高GC效率。
3. 垃圾收集算法详解
3.1 标记-清除算法
这是最基础的收集算法,分为两个阶段:
- 标记阶段:遍历所有GC Roots,标记存活对象
- 清除阶段:回收未被标记的对象占用的空间
问题:
- 效率问题:标记和清除两个过程效率都不高
- 空间问题:会产生大量不连续的内存碎片
// 模拟标记过程伪代码 void mark(Object root) { if (root == null || isMarked(root)) return; markBit(root); // 标记对象 for (Object ref : getReferences(root)) { mark(ref); // 递归标记引用对象 } }3.2 复制算法
为了解决标记-清除的效率问题,复制算法将内存分为两块,每次只使用一块。当一块用完时,将存活对象复制到另一块,然后清理已使用的内存空间。
优点:
- 没有内存碎片
- 实现简单,运行高效
缺点:
- 内存利用率只有50%
- 对象存活率高时复制开销大
注意:现代JVM的新生代通常采用改进的复制算法,将内存分为一个Eden区和两个Survivor区(8:1:1),这样只有10%的内存被闲置。
3.3 标记-整理算法
针对老年代的特点,标记-整理算法在标记完成后,让所有存活对象向一端移动,然后直接清理掉边界以外的内存。
优点:
- 没有内存碎片
- 适合老年代这种存活率高的区域
缺点:
- 移动对象成本高
- 需要暂停用户线程(Stop The World)
4. 经典垃圾收集器实现
4.1 Serial收集器
这是最基本的收集器,单线程工作,进行垃圾收集时必须暂停所有用户线程。它采用复制算法处理新生代,标记-整理算法处理老年代。
适用场景:
- 客户端模式下的JVM
- 内存资源受限的环境
- CPU核心数少的场景
4.2 Parallel Scavenge收集器
也称为吞吐量优先收集器,采用多线程并行收集,目标是达到可控制的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间))。
特点:
- 新生代使用复制算法
- 老年代使用标记-整理算法
- 适合后台运算而不需要太多交互的任务
4.3 CMS收集器
Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器,采用标记-清除算法。
工作流程:
- 初始标记(Stop The World)
- 并发标记
- 重新标记(Stop The World)
- 并发清除
优点:
- 并发收集,低停顿
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 会产生内存碎片
4.4 G1收集器
Garbage-First是一款面向服务端应用的收集器,主要针对多核CPU、大内存的机器。
创新点:
- 将堆划分为多个Region
- 优先回收价值最大的Region(Garbage-First名称由来)
- 可预测的停顿时间模型
工作流程:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
5. GC性能调优实战
5.1 关键JVM参数
| 参数 | 作用 | 推荐值 |
|---|---|---|
| -Xms | 初始堆大小 | 与-Xmx相同 |
| -Xmx | 最大堆大小 | 物理内存的1/4到1/2 |
| -Xmn | 新生代大小 | 整个堆的1/3到1/2 |
| -XX:SurvivorRatio | Eden与Survivor比例 | 8 |
| -XX:MaxTenuringThreshold | 晋升老年代年龄阈值 | 15 |
| -XX:+UseG1GC | 使用G1收集器 | - |
5.2 GC日志分析
启用GC日志的参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<filename>示例GC日志分析:
2023-07-20T14:23:45.731+0800: [GC (Allocation Failure) [PSYoungGen: 65536K->10752K(76288K)] 65536K->22016K(251392K), 0.0123456 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]- Allocation Failure:触发GC的原因(分配失败)
- PSYoungGen:Parallel Scavenge收集器的新生代GC
- 65536K->10752K:GC前后新生代使用量
- 65536K->22016K:GC前后整个堆使用量
- 0.0123456 secs:GC耗时
5.3 常见GC问题排查
频繁Full GC:
- 检查老年代空间是否过小
- 检查是否有内存泄漏
- 检查大对象分配
长时间GC停顿:
- 考虑使用G1或ZGC等低延迟收集器
- 检查是否有外部资源(如数据库)导致停顿
内存泄漏定位:
- 使用jmap生成堆转储文件
- 使用MAT或VisualVM分析对象引用链
# 生成堆转储文件 jmap -dump:format=b,file=heap.hprof <pid>6. 新一代垃圾收集器展望
6.1 ZGC收集器
ZGC(Z Garbage Collector)是JDK 11引入的实验性低延迟收集器,目标是在数TB的堆上保持低于10ms的停顿时间。
关键技术:
- 着色指针(Colored Pointers)
- 读屏障(Load Barrier)
- 并发处理所有阶段
6.2 Shenandoah收集器
与ZGC类似,Shenandoah也是低延迟收集器,通过Brooks指针等技术实现并发压缩。
与ZGC的主要区别:
- 采用转发指针而非着色指针
- 更注重吞吐量与延迟的平衡
6.3 选择收集器的考量因素
应用特性:
- 吞吐量优先还是延迟敏感
- 堆内存大小
- 对象分配速率
硬件资源:
- CPU核心数
- 内存带宽
SLA要求:
- 最大容忍停顿时间
- 允许的吞吐量损失
在实际项目中,我通常会先用G1作为默认选择,对于特别敏感的延迟要求会考虑ZGC。但要注意,新收集器可能需要更高版本的JDK和特定的硬件支持。