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)

这是最基础的算法,其他所有算法都是以这个作为基础来改进的

执行过程

  1. 标记:遍历所有 GC Roots 对象,标记所有可达对象(不是垃圾的才标记)
  2. 清除:遍历堆中的所有对象,未标记的就删除

特点

  • 两次遍历,一次遍历 GC Roots,一次遍历堆内存
  • 内存分布在初始本来是比较连续的,但是清除后可能大量不连续。这就是缺点:内存碎片

2. 标记-整理(Mark-Compact)

解决了内存碎片的问题,但是移动对象成本高。也有 标记-压缩 的叫法

执行过程

  1. 标记:同 标记-清除 的标记效果一次,先标记可达对象
  2. 整理:移动存活对象到一端。具体是移动可达对象,将可达对象向左紧凑排列,并覆盖垃圾对象
# 标记后
[ 对象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),内存回收流程如下:

  1. GC Roots 触发标记 Eden 和 From 区的存活对象
  2. Eden 和 From 区的存活对象复制到 To 区(复制算法)
  3. 把 To 区的对象复制到 From 区(为什么要交换?统计对象年龄,达到阈值对象进入老年代)

老年代

老年代空间大,占整个堆的2/3,对象存活时间比较长,回收效果没有新生代那么显著,所以更适合【标记-整理】算法

GC 区域和分类

新生代频繁发生、老年代很少发生、几乎不回收方法区

垃圾回收时所有线程会暂停

Minor GC(新生代回收)

  • 作用区域:仅回收新生代(Young Generation)
  • 触发条件:Eden 区空间不足时(Survivor 区内存不足不会触发)
  • 执行频率:最高(通常占所有 GC 的 90% 以上)

Major GC(老年代回收)

  • 作用区域:仅回收老年代(Old Generation)
  • 触发条件:老年代空间不足
  • 执行频率:较低(取决于对象晋升速度)

Minor GC 过程

  1. 回收 Eden 区:Eden 区存活的对象复制到 To 区,对象年龄=1
  2. 回收 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时间 短(毫秒级) 中等(百毫秒级) 长(秒级)
算法 复制 标记-清除/整理 混合算法
触发频率 低(应尽量避免)

对象生命周期

  1. 对象A分配在 Eden
  2. 发生 Minor GC 后:如果是垃圾就被回收,如果不是就从 Eden 复制到 To(年龄1)
  3. 再次发生 Minor GC 后:如果是垃圾就被回收,如果不是就从 To 复制到 From(年龄+1)
  4. 多次 Minor GC 后对象A可能依然存活(会在 From 和 To 反复复制,每复制一次,年龄+1)
  5. 当 对象A 经历 15 次 Minor GC 后,晋级到老年代
  6. 如果老年代发生 Major GC,对象A依然存活就不管他,如果是垃圾就被回收
  7. 如果发生 Full GC 对象A还是存活,也不管他,如果是垃圾就被回收

new 的对象如果太大(可配置),会直接进入老年代;标量替换优化可能不会创建对象

posted @ 2024-10-11 12:51  CyrusHuang  阅读(20)  评论(0)    收藏  举报