JVM 的内存结构

JVM 的内存区域按功能分为多个部分,其中堆(Heap) 和方法区(Method Area,JDK8 后为元空间 Metaspace) 是垃圾回收的主要目标(因为这两个区域的内存分配和释放是动态的)。其他区域(如程序计数器、虚拟机栈、本地方法栈)是线程私有且生命周期与线程一致,无需 GC(GC 就是垃圾回收)。

  • 堆:存储对象实例(几乎存储所有对象都在这里分配),是 GC 最核心的区域。堆按对象存活周期可分为:
    • 年轻代:分为 Eden 区和两个 Survivoe 区,用于存储新创建的对象,对象存活率低,回收频繁
    • 老年代:存储存活时间较长的对象(多次年轻代回收仍然存活的对象就会被存储到老年代),存活率高,回收频率低
  • 方法区:存储类信息,常量、静态变量,也会发生垃圾回收

垃圾回收

垃圾回收的本质是自动释放不再被使用地内存,解决手动管理内存容易出现的内存泄漏、内存溢出问题。我们需要考虑两个问题:哪些对象是垃圾?如何高效地回收这些垃圾。


  • STW:在垃圾回收的过程中,用户所有线程暂停执行
  • 内存分配策略:对象优先在 Eden 区分配;Eden 区满时触发 Minor GC(年轻代回收);多次 Minor GC 后仍存活的对象进入老年代;大对象直接进入老年代(避免频繁复制)。
  • 类卸载:方法区的垃圾回收主要是 “类卸载”,当该类的实例已经被回收,该类就会被回收

判断垃圾:JVM 需要先确定哪些对象已经死亡

  • 引用计数法:给每个对象设置一个引用计数器,被引用时+1,引用失效-1;当计数器为 0 时,认为对象可以回收。但是当 A 引用 B,B 引用 A 时,两者都不再被其他对象引用,计数器永远不为 0,导致无法回收。
  • 可达性分析算法:以 “GC Roots” 为起点,向下搜索引用链(对象之间的引用关系形成的链条);所有能被 GC Roots 直接或间接引用的对象都是 “存活的”,否则为 “垃圾”。GC Roots 必须时全局可访问且不会被回收的对象。
    若对象 A 被 GC Roots 引用,A 引用 B,B 引用 C,则 A、B、C 均存活;若 A 不再被 GC Roots 引用,则 A、B、C 均为垃圾。
    引用又分为 4 种强度:
  • 强引用:最普通的引用(如 Object obj = new Object()),只要强引用存在,对象永远不会被回收。
  • 软引用:用 SoftReference 包装,内存不足时才会被回收(适合缓存场景)。
  • 弱引用:用 SoftReference 包装,内存不足时才会被回收(适合缓存场景)。
  • 虚引用:用 PhantomReference 包装,唯一作用是在对象被回收时收到通知

垃圾回收算法

清除算法

通过可达性分析标记所有存活对象;遍历内存,回收所有未被标记的对象
优点:简单直接,无需移动对象
缺点:回收后空间不连续,大对象可能无法分配;效率低,标记和清除都需要遍历所有对象

复制算法

将内存分为大小相等的两块,只使用其中的一块;标记存活对象后,将所有存活对象复制到另一块,然后清空 A 块;
优点:复制后对象连续存储,无内存碎片;效率高,只需复制存活对象
缺点:内存利用率低,复制成本高

整理算法

标记存活对象,将所有对象整理到内存一端,然后直接清除边界以外的所有垃圾
优点:无内存碎片,内存利用率高;
缺点:整理需要移动对象,成本高

分代收集算法

将对象分为年轻代和老年代,年轻代用复制算法回收,老年代用清除、整理来回收

JVM 垃圾收集器

垃圾回收器时垃圾回收算法的具体实现