JVM 垃圾回收机制详解
一、垃圾回收概述
JVM的垃圾回收(Garbage Collection,GC)是Java内存管理的核心机制,它能自动检测并回收不再使用的内存空间,从而避免手动管理内存带来的内存泄漏和悬空指针问题。
二、如何确定对象需要回收
1. 引用计数法(Reference Counting)
- 原理:为每个对象维护一个计数器,记录有多少引用指向它。计数器为0时表示可回收。
- 缺点:无法解决循环引用问题(A引用B,B引用A,但都不可达)
- JVM未采用此方法
2. 可达性分析算法(Reachability Analysis)
JVM实际采用的算法,通过GC Roots作为起点向下搜索,走过的路径称为引用链(Reference Chain),不在链上的对象即可回收。
GC Roots包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- Java虚拟机内部的引用(基本数据类型对应的Class对象、常驻的异常对象等)
- 所有被同步锁(synchronized)持有的对象
3. 引用类型对回收的影响
| 引用类型 | 回收时机 | 用途 |
|---|---|---|
| 强引用(Strong Reference) | 永不回收(只要引用存在) | 普通对象引用 |
| 软引用(Soft Reference) | 内存不足时回收 | 内存敏感缓存 |
| 弱引用(Weak Reference) | 下次GC时回收 | WeakHashMap、ThreadLocal |
| 虚引用(Phantom Reference) | 任何时候都可能回收 | 对象回收跟踪 |
4. finalize() 方法
- 对象被回收前会调用finalize()方法(但不保证执行完成)
- 可以在finalize()中重新建立引用,逃脱回收(只能自救一次)
- JDK9开始已标记为弃用,不推荐使用
三、垃圾收集算法
1. 标记-清除(Mark-Sweep)
流程:标记所有需要回收的对象 → 统一清除
优点:实现简单
缺点:产生大量内存碎片;效率不稳定
2. 标记-复制(Mark-Copy)
makefile
流程:将内存分为两块 → 使用其中一块 → 存活对象复制到另一块 → 清理原区域
优点:无碎片,效率高(针对存活对象少的情况)
缺点:内存利用率只有50%
应用:新生代(Eden和Survivor区比例8:1:1)
3. 标记-整理(Mark-Compact)
vbnet
流程:标记存活对象 → 将所有存活对象向一端移动 → 清理边界外内存
优点:无碎片,内存利用率高
缺点:移动对象需要暂停应用(Stop-The-World)
应用:老年代
4. 分代收集(Generational Collection)
结合不同算法的优势:
- 新生代:对象存活率低 → 标记-复制算法
- 老年代:对象存活率高 → 标记-清除或标记-整理算法
四、JVM内存分代模型
scss
┌─────────────────────────────────────────┐
│ 堆内存(Heap) │
├─────────────┬─────────────┬─────────────┤
│ 新生代(1/3) │ 老年代(2/3) │ 元空间(非堆) │
├─────┬────┬──┤ │ │
│Eden │ S0 │S1│ │ │
│ 8/10│1/10│1/10 │ │
└─────┴────┴──┴─────────────┴─────────────┘
对象晋升流程
- 新对象优先在Eden区分配
- Eden区满时触发Minor GC
- 存活对象复制到Survivor区(年龄+1)
- 年龄达到阈值(默认15)晋升到老年代
- 大对象直接进入老年代(-XX:PretenureSizeThreshold)
五、常见垃圾收集器
1. Serial / Serial Old
- 单线程收集器,GC时会暂停所有工作线程
- 适合单核CPU、客户端模式(内存<100MB)
- 参数:
-XX:+UseSerialGC
2. Parallel Scavenge / Parallel Old(吞吐量优先)
- 多线程并行收集,JDK8默认
- 关注可控吞吐量(用户代码运行时间/总时间)
- 参数:
-XX:+UseParallelGC
3. ParNew
- Parallel Scavenge的变种,用于与CMS配合
- 参数:
-XX:+UseParNewGC
4. CMS(Concurrent Mark Sweep)
- 目标:最小化停顿时间
- 流程:初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除
- 缺点:产生内存碎片、浮动垃圾、CPU敏感
- JDK9开始弃用,JDK14完全移除
5. G1(Garbage First)
- 特点 :
- 将堆划分为多个相等大小的Region(1-32MB)
- 优先回收垃圾最多的Region
- 可预测的停顿时间模型
- 流程:初始标记 → 并发标记 → 最终标记 → 筛选回收
- 适用:多核大内存(>6GB)、要求低延迟的场景
- 参数:
-XX:+UseG1GC
6. ZGC(Z Garbage Collector)
- JDK11引入,JDK15正式生产可用
- 特点:停顿时间<10ms,支持TB级大堆
- 核心技术:着色指针、读屏障、并发整理
- 参数:
-XX:+UseZGC
7. Shenandoah
- 与ZGC类似,RedHat贡献,JDK12引入
- 特点:并发压缩,与ZGC的主要竞争
六、GC类型与触发时机
| GC类型 | 触发区域 | 触发时机 | 影响 |
|---|---|---|---|
| Minor GC / Young GC | 新生代 | Eden区满 | 短暂停顿(通常<10ms) |
| Major GC / Old GC | 老年代 | CMS GC或老年代空间不足 | 停顿时间较长 |
| Mixed GC(G1) | 所有年轻代+部分老年代 | G1中老年代占比达到阈值 | 停顿可控 |
| Full GC | 整个堆+元空间 | 老年代满、System.gc()、元空间满 | 严重停顿 |
七、性能调优关键参数
bash
# 典型G1配置示例
-Xms8G -Xmx8G # 堆内存大小
-XX:+UseG1GC # 使用G1收集器
-XX:MaxGCPauseMillis=200 # 目标停顿时间200ms
-XX:G1NewSizePercent=5 # 年轻代初始占比5%
-XX:G1HeapRegionSize=16M # Region大小16MB
-XX:+PrintGCDetails # 打印GC详情
-XX:+PrintGCDateStamps # 打印GC时间戳
-Xloggc:/path/to/gc.log # GC日志路径
八、性能调优建议
-
观察指标:
- GC频率和停顿时间
- 吞吐量(应用程序运行时间/总时间)
- 内存占用(堆使用率、各代使用情况)
-
常见问题:
- 频繁Minor GC:年轻代过小 → 增大新生代
- 频繁Full GC:内存泄漏或老年代过小 → 检查内存、增大堆
- GC停顿过长:收集器不合适 → 切换G1/ZGC、调优并发线程数
-
调试工具:
- jstat:监控GC统计信息
- jmap + MAT:分析堆转储文件
- GC日志分析工具:GCeasy、GCViewer
- VisualVM、JProfiler:可视化监控
九、开发人员最佳实践
- 减少对象创建:对象复用、避免循环内创建对象
- 及时释放引用:显式置null(针对大对象、静态集合)
- 合理使用引用类型:缓存使用软引用/弱引用
- 避免手动调用System.gc():会触发Full GC
- 注意finalize()陷阱:影响GC效率,不要依赖
- 大对象处理:避免短期大对象,考虑对象池
十、总结
JVM垃圾回收的核心是分代收集 和并发回收 的平衡。选择合适的垃圾收集器,关键取决于应用对吞吐量 和延迟的要求:
- 批处理任务 → Parallel GC(高吞吐量)
- Web服务/微服务 → G1 GC(平衡吞吐量和延迟)
- 低延迟交易系统 → ZGC/Shenandoah(极致低停顿)
理解GC机制不仅能避免内存问题,更是JVM性能调优的基础。建议在实践中结合具体情况,通过监控数据和GC日志进行针对性优化。