1. 为什么需要垃圾回收机制
在Java的世界里,垃圾回收(Garbage Collection,简称GC)就像一位不知疲倦的清洁工,默默帮我们处理那些不再使用的内存对象。想象一下,如果你在Java程序中创建了成千上万个对象,但从不清理那些已经完成使命的对象,内存很快就会被耗尽——这就是著名的"内存泄漏"问题。
Java虚拟机(JVM)的自动内存管理机制,让开发者从繁琐的手动内存管理中解放出来。与C/C++等语言不同,Java程序员不需要(也不能)直接调用类似free()或delete这样的操作来释放内存。这种设计大大降低了内存管理错误的可能性,但同时也带来了新的挑战:如何高效地识别和回收垃圾对象?
提示:虽然GC自动运行,但不代表我们可以完全忽视内存管理。不当的对象创建和引用仍然会导致内存问题。
2. 垃圾回收的基本原理
2.1 对象存活的判定标准
JVM判断对象是否存活的核心依据是可达性分析(Reachability Analysis)。简单来说,如果一个对象不能被任何"GC Roots"对象通过引用链访问到,那它就被认为是垃圾。
GC Roots通常包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- Java虚拟机内部的引用(如基本类型对应的Class对象)
2.2 四种引用类型
Java设计了不同强度的引用类型,让开发者可以更灵活地控制对象的生命周期:
强引用(Strong Reference):最常见的引用类型,只要强引用存在,对象就永远不会被回收。
Object obj = new Object(); // 强引用软引用(Soft Reference):内存不足时会被回收,适合用于缓存。
SoftReference<Object> softRef = new SoftReference<>(new Object());弱引用(Weak Reference):只要发生GC就会被回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object());虚引用(Phantom Reference):最弱的引用,主要用于跟踪对象被回收的活动。
3. 经典垃圾回收算法
3.1 标记-清除算法(Mark-Sweep)
这是最基础的GC算法,分为两个阶段:
- 标记阶段:遍历所有GC Roots,标记可达对象
- 清除阶段:回收未被标记的对象占用的内存
问题:
- 产生内存碎片
- 执行效率不稳定(对象越多,耗时越长)
3.2 复制算法(Copying)
将内存分为大小相等的两块,每次只使用其中一块。当这块内存用完时,将存活的对象复制到另一块,然后清除已使用的内存空间。
优点:
- 没有内存碎片
- 分配内存简单(只需要移动堆顶指针)
缺点:
- 内存利用率只有50%
- 对象存活率高时复制开销大
3.3 标记-整理算法(Mark-Compact)
结合了标记-清除和复制算法的优点:
- 标记阶段与标记-清除相同
- 整理阶段将所有存活对象向一端移动,然后清理边界外的内存
适用场景:
- 老年代回收(对象存活率高)
- 对内存碎片敏感的场景
3.4 分代收集理论
现代JVM普遍采用分代收集策略,基于以下经验法则:
- 绝大多数对象都是"朝生夕死"的
- 熬过越多次GC的对象,越不容易死亡
因此,JVM将堆内存划分为:
- 新生代(Young Generation):存放新创建的对象
- 老年代(Old Generation):存放长期存活的对象
- 永久代/元空间(PermGen/Metaspace):存放类元数据等(Java 8后改为Metaspace)
4. HotSpot JVM的分代实现
4.1 新生代回收(Minor GC)
新生代通常采用复制算法,并进一步划分为:
- Eden区:新对象分配的区域
- Survivor区(From/To):存放Minor GC后存活的对象
工作流程:
- 新对象分配在Eden区
- Eden区满时触发Minor GC
- 存活对象被复制到Survivor区的To空间
- 年龄计数器+1
- From和To空间角色交换
注意:当对象年龄达到阈值(默认15)时,会晋升到老年代。大对象也可能直接进入老年代。
4.2 老年代回收(Major GC/Full GC)
老年代通常采用标记-清除或标记-整理算法。触发条件包括:
- 老年代空间不足
- 永久代/元空间不足
- System.gc()调用(不建议使用)
- CMS GC的并发模式失败
Full GC会暂停所有应用线程(Stop-The-World),对性能影响较大。
5. G1垃圾收集器深度解析
5.1 G1的设计理念
G1(Garbage-First)是JDK 9及以后版本的默认垃圾收集器,其核心思想是:
- 将堆划分为多个大小相等的Region(默认约2048个)
- 优先回收垃圾最多的Region(Garbage-First)
- 可预测的停顿时间模型
5.2 G1的内存布局
G1将堆划分为:
- Eden Regions
- Survivor Regions
- Old Regions
- Humongous Regions(存放大对象)
- 空闲Regions
每个Region的大小可以通过-XX:G1HeapRegionSize参数指定(1MB~32MB)。
5.3 G1的工作流程
- 初始标记(Initial Mark):标记GC Roots直接关联的对象(需要STW)
- 并发标记(Concurrent Mark):从GC Roots开始标记所有可达对象
- 最终标记(Remark):处理并发标记期间的变化(需要STW)
- 筛选回收(Cleanup):统计各Region的回收价值,选择收益最高的Region进行回收
5.4 G1的关键参数
| 参数 | 说明 | 推荐值 |
|---|---|---|
| -XX:+UseG1GC | 启用G1收集器 | 必选 |
| -XX:MaxGCPauseMillis | 目标最大停顿时间 | 200ms |
| -XX:InitiatingHeapOccupancyPercent | 触发并发GC周期的堆占用率阈值 | 45 |
| -XX:G1NewSizePercent | 新生代最小占比 | 5 |
| -XX:G1MaxNewSizePercent | 新生代最大占比 | 60 |
6. 垃圾回收性能调优实战
6.1 选择合适的GC收集器
根据应用特点选择GC:
- 吞吐量优先:Parallel Scavenge + Parallel Old
- 低延迟优先:CMS或G1
- 大内存服务:ZGC或Shenandoah(JDK 11+)
6.2 常见问题排查
问题1:频繁Full GC可能原因:
- 老年代空间不足
- 永久代/元空间不足
- 内存泄漏
排查工具:
- jstat -gcutil
- jmap -histo
- Memory Analyzer Tool (MAT)
问题2:GC停顿时间过长解决方案:
- 调整-XX:MaxGCPauseMillis
- 增加堆大小
- 优化对象分配模式
6.3 JVM参数配置示例
对于8GB内存的Web服务:
-Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1ReservePercent=10 -XX:ConcGCThreads=47. 垃圾回收的未来发展
随着硬件发展和大内存应用的普及,新一代GC技术不断涌现:
ZGC:JDK 11引入,目标停顿时间不超过10ms
- 基于染色指针和读屏障
- 支持TB级堆内存
Shenandoah:低停顿时间的并发收集器
- 并发压缩
- 与G1类似的分Region设计
Epsilon GC:实验性"无操作"收集器
- 适合短期运行的应用
- 用于性能测试和极短生命周期的应用
在实际项目中,我发现很多性能问题都源于对GC机制的理解不足。比如曾经遇到一个案例:一个简单的缓存实现使用了强引用,导致老年代快速填满引发频繁Full GC。改为软引用后,系统稳定性显著提升。这也提醒我们,虽然JVM提供了自动内存管理,但开发者仍需理解其工作原理才能写出高性能的应用。