JVM垃圾回收机制
大约 8 分钟
JVM垃圾回收机制
核心理论
1.1 垃圾回收基本概念
垃圾回收(Garbage Collection, GC)是JVM自动管理内存的机制,主要负责:
- 识别内存中不再使用的对象(垃圾)
- 回收这些对象占用的内存空间
- 整理内存碎片(可选)
垃圾回收的目标是实现内存自动管理,减少内存泄漏和内存溢出问题,提高开发效率。
1.2 对象存活判定算法
1.2.1 引用计数法
- 原理:为每个对象添加引用计数器,当对象被引用时计数器加1,引用失效时减1,计数器为0的对象可回收
- 优点:实现简单,判定效率高
- 缺点:无法解决循环引用问题(如两个对象互相引用但都不再被其他对象引用)
1.2.2 可达性分析算法
- 原理:以"GC Roots"为起点,向下搜索所有可达的对象,不可达的对象即为可回收对象
- GC Roots包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象
- 优点:可解决循环引用问题,是JVM实际采用的算法
1.3 垃圾回收算法
1.3.1 标记-清除算法(Mark-Sweep)
- 过程:标记所有需要回收的对象,然后统一回收被标记的对象
- 优点:实现简单
- 缺点:产生内存碎片,导致大对象无法分配内存
1.3.2 标记-复制算法(Mark-Copy)
- 过程:将内存分为大小相等的两块,每次只使用一块,回收时将存活对象复制到另一块,然后清除使用过的内存块
- 优点:无内存碎片,实现简单
- 缺点:内存利用率低(仅50%),复制成本高
- 应用:新生代垃圾回收(如Serial、ParNew收集器)
1.3.3 标记-整理算法(Mark-Compact)
- 过程:标记存活对象,然后将存活对象向一端移动,最后清除边界以外的内存
- 优点:无内存碎片,内存利用率高
- 缺点:整理过程成本高
- 应用:老年代垃圾回收(如Serial Old、Parallel Old收集器)
1.3.4 分代收集算法
- 原理:根据对象存活周期将内存划分为不同区域(新生代、老年代、永久代/元空间),对不同区域采用不同回收算法
- 新生代:对象存活时间短,采用标记-复制算法
- 老年代:对象存活时间长,采用标记-清除或标记-整理算法
- 优点:结合了不同算法的优势,提高回收效率
1.4 垃圾收集器
JVM提供了多种垃圾收集器,各有特点:
1.4.1 新生代收集器
- Serial收集器:单线程收集,简单高效,适用于Client模式
- ParNew收集器:Serial的多线程版本,可与CMS配合使用
- Parallel Scavenge收集器:关注吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间)),支持自适应调节策略
1.4.2 老年代收集器
- Serial Old收集器:Serial的老年代版本,单线程标记-整理算法
- Parallel Old收集器:Parallel Scavenge的老年代版本,多线程标记-整理算法
- CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间为目标,基于标记-清除算法,并发收集、低停顿
- G1(Garbage-First)收集器:面向服务端应用,兼顾吞吐量和延迟,基于Region的分代式垃圾收集器
代码实践
2.1 查看JVM默认垃圾收集器
public class GCCollectorInfo {
public static void main(String[] args) {
// 获取新生代垃圾收集器
String youngCollector = System.getProperty("sun.java.command");
System.out.println("JVM参数: " + youngCollector);
// 获取新生代垃圾收集器
String youngGC = System.getProperty("sun年轻代收集器");
// 获取老年代垃圾收集器
String oldGC = System.getProperty("sun老年代收集器");
System.out.println("新生代收集器: " + youngGC);
System.out.println("老年代收集器: " + oldGC);
}
}
2.2 手动触发垃圾回收(不推荐在生产环境使用)
public class GCTriggerDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Object obj = new Object();
System.out.println("创建对象: " + obj);
// 手动触发垃圾回收(仅为演示,生产环境不推荐)
System.gc();
}
}
}
2.3 分析GC日志
添加JVM参数打印GC日志:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
GC日志示例分析:
2023-10-01T12:00:00.123+0800: [GC (Allocation Failure) [PSYoungGen: 524288K->65536K(786432K)] 524288K->131072K(2097152K), 0.0123450 secs] [Times: user=0.02 sys=0.01, real=0.01 secs]
- PSYoungGen:使用Parallel Scavenge收集器
- 524288K->65536K(786432K):新生代GC前使用容量->GC后使用容量(新生代总容量)
- 524288K->131072K(2097152K):整个堆GC前使用容量->GC后使用容量(堆总容量)
- 0.0123450 secs:GC耗时
设计思想
3.1 分代回收的设计理念
分代回收基于对象存活周期的经验法则:
- 大部分对象存活时间短(朝生夕死)
- 存活下来的对象更可能长时间存活
基于这一法则,将内存划分为新生代和老年代,对不同代采用不同回收策略,提高回收效率。新生代区域小、回收频繁,采用高效的标记-复制算法;老年代区域大、回收频率低,采用标记-清除或标记-整理算法。
3.2 CMS收集器的并发设计
CMS收集器以低延迟为目标,采用并发设计:
- 初始标记:暂停所有用户线程,标记GC Roots直接关联的对象(速度快)
- 并发标记:恢复用户线程,同时标记所有可达对象(耗时,但并发执行)
- 重新标记:暂停所有用户线程,修正并发标记期间因用户线程操作导致标记变动的对象(比初始标记稍长,但比并发标记短)
- 并发清除:恢复用户线程,同时清除标记的垃圾对象(耗时,但并发执行)
通过减少暂停时间,CMS适合对响应时间要求高的应用。
3.3 G1收集器的Region化内存布局
G1收集器将堆内存划分为多个大小相等的独立Region,每个Region可以根据需要扮演新生代的Eden区、Survivor区或老年代空间。这种设计允许G1跟踪各个Region的垃圾堆积价值,优先回收价值最高的Region(Garbage-First),从而在有限时间内获得最高的回收效率。
避坑指南
4.1 不要过度依赖System.gc()
- System.gc()只是建议JVM进行垃圾回收,JVM可以忽略该请求
- 频繁调用会影响性能,增加GC overhead
- 生产环境应禁用显式GC:-XX:+DisableExplicitGC
4.2 避免内存泄漏
- 静态集合类泄漏:静态集合持有对象引用,导致对象无法回收
public class StaticCollectionLeak { private static List<Object> list = new ArrayList<>(); public void add(Object obj) { list.add(obj); // obj永远不会被回收 } }
- 监听器和回调泄漏:注册监听器但未注销
- 资源未关闭泄漏:数据库连接、IO流等资源未关闭
4.3 合理设置堆内存大小
- 堆内存过小:频繁GC,甚至OOM
- 堆内存过大:单次GC时间过长,浪费系统资源
- 建议:根据应用实际需求和服务器配置设置,新生代和老年代比例一般为1:2
4.4 CMS收集器的常见问题
- 内存碎片:基于标记-清除算法,长期运行会产生内存碎片 解决:开启-XX:+UseCMSCompactAtFullCollection,在Full GC后进行内存整理
- Concurrent Mode Failure:并发清除阶段用户线程分配内存速度超过GC回收速度 解决:增大老年代空间,或使用G1收集器替代
深度思考题
- G1收集器与CMS收集器相比有哪些优势?适用于什么场景?
- 什么是内存分配担保机制?它在垃圾回收中起到什么作用?
- 如何排查和解决JVM内存泄漏问题?
思考题回答:
G1收集器的优势:
- 基于Region的内存布局,可预测的停顿时间
- 兼顾吞吐量和延迟
- 不会产生大量内存碎片
- 可动态调整新生代和老年代大小 适用场景:堆内存较大(一般大于4GB)、对停顿时间有要求的应用
内存分配担保机制是指当新生代无法为新对象分配内存时,JVM会检查老年代最大可用连续空间是否大于新生代所有对象总大小。如果大于,则进行Minor GC;如果小于,则查看HandlePromotionFailure参数是否允许担保失败。如果允许,则尝试Minor GC;如果不允许,则进行Full GC。内存分配担保机制是为了减少Full GC的频率。
排查和解决JVM内存泄漏问题的步骤:
- 监控JVM内存使用情况,观察是否有内存持续增长
- 发生OOM时,通过-XX:+HeapDumpOnOutOfMemoryError参数获取堆转储文件
- 使用MAT、JProfiler等工具分析堆转储文件,找出泄漏对象
- 分析泄漏对象的引用链,确定泄漏原因
- 修改代码,解除不必要的对象引用