JVM 垃圾回收
GC 算法
一、垃圾判定方法
1. 引用计数法(Reference Counting)
原理:
- 每个对象维护一个引用计数器
- 当被引用时计数器+1,引用失效时-1
- 计数器=0时判定为垃圾
示例:
// 针对 a 对象
Object a = new Object(); // 计数=1
Object b = a; // 计数=2
a = null; // 计数=1
b = null; // 计数=0 → 可回收
优缺点:
- ✅ 简单高效
- ❌ 无法解决循环引用问题
- ❌ 计数器维护开销大
循环引用:
对象已经指向 null(是垃圾)但计数器不是 0,所以不会回收。这就是内存泄漏
// 步骤1:创建对象
Node a = new Node(); // a 的计数器=1
Node b = new Node(); // b 的计数器=1
// 步骤2:建立循环引用
a.next = b; // b 的计数器=2
b.next = a; // a 的计数器=2
// 步骤3:断开外部引用
a = null; // a 的计数器-1,结果是1,但对象已经是 null 了
2. 可达性分析(GC Roots Tracing)
原理:
- 从GC Roots对象作为起点
- 通过引用链遍历所有可达对象
- 未被标记的对象判定为垃圾
GC Roots包括:
- 虚拟机栈中的引用对象
- 方法区静态属性引用对象
- 方法区常量引用对象
- Native方法引用的对象
- 同步锁持有的对象
特点
可达性分析算法更安全,不会存在内存泄漏
但为了精确,在分析对象可达性时线程要暂停,是所有的线程,这就是 STW(stop the world)
二、垃圾回收算法
1. 标记-清除(Mark-Sweep)
这是最基础的算法,其他所有算法都是以这个作为基础来改进的
执行过程:
- 标记:遍历所有 GC Roots 对象,标记所有可达对象(不是垃圾的才标记)
- 清除:遍历堆中的所有对象,未标记的就删除
特点:
- 两次遍历,一次遍历 GC Roots,一次遍历堆内存
- 内存分布在初始本来是比较连续的,但是清除后可能大量不连续。这就是缺点:内存碎片
2. 标记-整理(Mark-Compact)
解决了内存碎片的问题,但是移动对象成本高。也有 标记-压缩 的叫法
执行过程:
- 标记:同 标记-清除 的标记效果一次,先标记可达对象
- 整理:移动存活对象到一端。具体是移动可达对象,将可达对象向左紧凑排列,并覆盖垃圾对象
# 标记后
[ 对象A(垃圾) | 对象B(非垃圾) | 对象C(垃圾) | 对象D(非垃圾) ]
# 整理后
[ 对象B(非垃圾) | 对象D(非垃圾) | 对象C(垃圾) | 空闲内存 ]
特点:
- 移动对象能解决内存碎片问题,但成本高(时间换空间)
- 整理后垃圾对象可能还存在于堆中,但是这块内存是可用的,下次分配时新对象直接覆盖这块区域
3. 复制算法(Copying)
执行过程:遍历 GC Roots 对象,把 from 区的可达对象复制到 to 区
特点:
- 将内存分为大小相等的两块,每次只使用其中一块
- 优点是无碎片,高效;缺点是内存利用率仅50%(空间换时间)
- 遍历 GC Roots 时不会标记可达对象
4. 总结
- 标记-清除 算法内存碎片太严重,现代垃圾收集器不会直接使用了(CMS 在 JDK1.4 - JDK1.8 期间还是主流,是因为当时没有好的替代方案)
- 标记-整理 算法时间换空间,适用于空间更重要的场景
- 复制 算法空间换时间,适用于时间更重要的场景
三、算法应用
新生代
新生代因为发生 GC 的频率比较高,相比来说更看重时间,所以新生代适合【复制】算法
新生代分为伊甸园区和两个幸存区(内存大小比例:8:1:1
),内存回收流程如下:
- GC Roots 触发标记 Eden 和 From 区的存活对象
- Eden 和 From 区的存活对象复制到 To 区(复制算法)
- 把 To 区的对象复制到 From 区(为什么要交换?统计对象年龄,达到阈值对象进入老年代)
老年代
老年代空间大,占整个堆的2/3,对象存活时间比较长,回收效果没有新生代那么显著,所以更适合【标记-整理】算法
GC 区域和分类
新生代频繁发生、老年代很少发生、几乎不回收方法区
垃圾回收时所有线程会暂停
Minor GC(新生代回收)
- 作用区域:仅回收新生代(Young Generation)
- 触发条件:Eden 区空间不足时(Survivor 区内存不足不会触发)
- 执行频率:最高(通常占所有 GC 的 90% 以上)
Major GC(老年代回收)
- 作用区域:仅回收老年代(Old Generation)
- 触发条件:老年代空间不足
- 执行频率:较低(取决于对象晋升速度)
Minor GC 过程
- 回收 Eden 区:Eden 区存活的对象复制到 To 区,对象年龄=1
- 回收 From 区:From 区存活的对象复制到 To 区,对象年龄+1
这次 Minor GC 后,只有 To 区有对象,Eden 和 From 都是干净的(可能垃圾依然存在,但是这块内存是可用的)
下一次 Mino GC 时,回收 Eden 和 To 区,回收后只有 From 区有对象,Eden 和 To 是干净的(如此往复交换 From 和 To)
Full GC(整堆回收)
- 作用区域:同时回收新生代+老年代+方法区
- 触发条件:
- 老年代空间不足
- 方法区(元空间)空间不足
- System.gc()调用(不一定立即触发)
- 堆外内存分配失败
对比
特性 | Minor GC | Major GC | Full GC |
---|---|---|---|
作用区域 | 新生代 | 老年代 | 整个堆+方法区 |
STW时间 | 短(毫秒级) | 中等(百毫秒级) | 长(秒级) |
算法 | 复制 | 标记-清除/整理 | 混合算法 |
触发频率 | 高 | 中 | 低(应尽量避免) |
对象生命周期
- 对象A分配在 Eden
- 发生 Minor GC 后:如果是垃圾就被回收,如果不是就从 Eden 复制到 To(年龄1)
- 再次发生 Minor GC 后:如果是垃圾就被回收,如果不是就从 To 复制到 From(年龄+1)
- 多次 Minor GC 后对象A可能依然存活(会在 From 和 To 反复复制,每复制一次,年龄+1)
- 当 对象A 经历 15 次 Minor GC 后,晋级到老年代
- 如果老年代发生 Major GC,对象A依然存活就不管他,如果是垃圾就被回收
- 如果发生 Full GC 对象A还是存活,也不管他,如果是垃圾就被回收
new 的对象如果太大(可配置),会直接进入老年代;标量替换优化可能不会创建对象