什么是垃圾回收

垃圾回收指的是:自动清除程序中不再被使用的对象所占用的内存空间。
JVM 会跟踪哪些对象仍然“可达”,哪些对象已经“不可达”,并自动释放不可达对象占用的内存。

内存泄漏:不再使用的对象在系统中未被回收。内存泄漏会导致内存溢出

  • 哪些内存区域会被回收?
区域是否 GC 管理
Java 堆(Heap)✅ 主要回收区
方法区(元空间)✅ 部分可回收(如类元信息)
栈、本地方法栈、程序计数器❌ 不受 GC 管理

方法区的回收

🔹二、什么情况下类信息可以被回收?
前提:类必须“不可达”才能被卸载。

JVM 会回收类的元信息,但 只有满足以下条件时,类才会被认为“无用”:

  1. 该类的所有实例都被回收;

  2. 加载该类的类加载器已被回收;

  3. 该类的 java.lang.Class 对象没有被引用。

👉 一句话:类加载器 + 类本身 + 所有实例都“不可达”,才能触发卸载和方法区回收。

使用的情况很少

堆的回收

如何判断对象是否可以回收

可达性分析算法

  • 基本思想:

从一组被称为 GC Roots 的根对象出发,沿着对象的引用链向下搜索,凡是能从 GC Roots 直接或间接访问到的对象都是“可达”的”,它们被认为“仍然活着”;而无法访问到的对象是“不可达”的”,就会被判定为垃圾,等待回收。

  • GC Roots(根对象)有哪些?
    GC Roots 是 JVM 内建的一组引用链起点,它们本身不会被垃圾回收,主要包括:
类型说明
虚拟机栈中的引用变量各个线程正在调用的方法中的局部变量表中的对象引用
方法区中的静态变量类的静态属性所引用的对象
方法区中的常量引用字面量引用的对象,如字符串常量池
本地方法栈中的 JNI 引用本地代码(C/C++)引用的对象
运行中线程对象本身正在执行的线程默认不会被回收
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    GC Roots
    |
    v
    [Obj1] ---> [Obj2] ---> [Obj3]

    [Obj4]

    [Obj5] ←→ [Obj6] ←→ [Obj7]
    ↑ ↓
    GC 无法访问 GC 无法访问
    • Obj1~4 是可达的对象 → 存活
    • Obj5~7 互相引用,但没有任何一个能被 GC Roots 访问 → 不可达 → 可回收
  • 可达性分析的执行步骤

  1. 从 GC Roots 开始,把所有可直接访问到的对象标记为“可达”;
  2. 递归遍历这些对象引用的其他对象,也标记为“可达”;
  3. 最终未被标记的对象即为“不可达”;
  4. 不可达对象会被 GC 回收,但如果有自我拯救机制(如 finalize()),可能暂时保留。

引用类型

引用类型回收条件应用场景
强引用(StrongReference)永远不会被回收(只要还被引用)普通对象引用
软引用(SoftReference)只有在内存不足时才会被回收缓存、图片缓存
弱引用(WeakReference)下一次 GC 时就会被回收,不论内存是否充足ThreadLocal 的 key
虚引用(PhantomReference)随时可能被回收,不能通过引用获取对象对象销毁后通知清理操作
  • 强引用(Strong Reference):是最常见的引用类型,也是默认的引用类型。如果一个对象具有强引用,那么即使内存空间不足,垃圾回收器也不会回收它。只有当该对象的所有强引用都失效时,对象才会被回收
  • 软引用(Soft Reference):是一种比强引用弱一些的引用类型。如果一个对象只具有软引用,那么当内存空间不足时,垃圾回收器可能会回收它。软引用通常用于实现内存敏感的缓存
    可以配合引用队列来释放软引用自身
  • 弱引用(Weak Reference):是一种比软引用更弱一些的引用类型。如果一个对象只具有弱银用,那么垃圾回收器在下一次运行时,无论内存空间是否足够,都会回收该对象。若引用通常用于实现在对象可用时进行操作的场景
    可以配合引用队列来释放软引用自身
  • 虚引用(Phantom Reference):是最弱的一种引用类型。如果一个对象只具有虚引用,那么在任何时候都可能被垃圾回收器回收。虚引用通常用于追踪对象被垃圾回收的状态
    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
  • 终结器引用(Final Reference):是一种特殊的弱引用类型,它只在对象被回收时被添加到引用队列中。当垃圾回收器准备回收一个对象时,会先执行对象的finallize()方法,如果finalize()方法中没有重新让对象与其他对象建立联系,那么这个对象就会被回收,并且它的Final引用会被加入到引用队列中。Final引用通常用于对象回收后的清理工作

垃圾回收算法

  • 常见的垃圾回收算法
算法名称简介特点
标记-清除(Mark-Sweep)标记所有存活对象,清除未标记的简单、会产生内存碎片
复制(Copying)将存活对象从一块内存复制到另一块快速、无碎片、适合年轻代
标记-整理(Mark-Compact)标记所有存活对象,并移动整理成连续空间适合老年代、无碎片,开销大
分代收集(Generational GC)按对象生命周期分代,采用不同算法收集实际应用最多,JVM 默认策略

标记清楚算法

标记清除算法的优点在于它简单易用,可以快速地回收大量的垃圾对象。但是,它也存在一些缺点,例如在清除和压缩阶段中可能会产生较大的内存碎片,从而影响后续的内存分配效率。此外,标记清除算法无法处理循环引用的情况,需要借助其他算法来处理循环引用问题,例如标记-压缩算法和复制算法等。
过程:

  • 标记阶段:从 GC Roots 出发,标记所有可达对象
  • 清除阶段:清除未被标记的对象

缺点:

  • 会产生 大量内存碎片
  • 分配新对象可能需要整理或空闲链查找

复制算法

复制算法是一种基于内存分区的垃圾回收算法,它将内存分成两个区域:From空间和To空间。在正常情况下,所有的对象都被分配在From空间中。当需要进行垃圾回收时,算法会扫描From空间中的所有对象,并将存活的对象复制到To空间中。复制完成后,From空间中的对象都可以被视为垃圾,并可以被回收。

  1. 接下来,详细介绍一下复制算法的工作原理:
  • 内存分配:在程序运行过程中,对象的内存分配只在From空间中进行。当From空间快要用完时,算法会触发一次垃圾回收操作。
  • 扫描存活对象:在进行垃圾回收时,算法会遍历From空间中的所有对象,并标记存活的对象。为了标记存活对象,复制算法使用了一种叫做可达性分析的技术,这个技术可以判断一个对象是否是存活对象。与标记清除算法和标记整理算法不同的是,复制算法并不需要进行标记和清除的分离过程,因为复制算法是将存活对象复制到To空间中,所以只要扫描完From空间中的所有对象,并将存活对象复制到To空间中,就可以直接清除From空间了。
  • 复制存活对象:在扫描存活对象之后,算法会将所有存活对象从From空间复制到To空间。复制的过程是按照对象的存活顺序进行的,也就是说,如果对象A引用了对象B,那么对象B会被先复制到To空间中。复制完成后,To空间的使用量会变得很满,From空间的使用量则变得很少。
  • 交换空间:在复制完所有存活对象之后,From空间中剩余的对象都可以视为垃圾,并可以被回收。为了保证下一次的内存分配,To空间和From空间会进行交换,也就是说,To空间成为了新的From空间,From空间成为了新的To空间。这样一来,内存分配就可以在新的From空间中进行了。

优点:

  • 快速、高效

  • 回收过程简单:只复制存活对象,然后清空原区域 → 无碎片

缺点:

  • 空间浪费大(总有一块 Survivor 区是空的)

  • 复制成本高(如果存活对象多)

    标记整理算法

    标记-整理算法的流程分为两步:

  • 标记(Mark)阶段:从 GC Roots 出发,标记所有“存活对象”;
  • 整理(Compact)阶段:将所有存活对象移动到一端,并按顺序排列,清理无用对象所占内存。

分代收集算法

JVM(Java虚拟机)的分代垃圾回收是一种优化内存回收的技术。它利用对象的生命周期来将堆(heap)分为不同的区域,然后针对不同区域的特点采用不同的垃圾回收算法。

  • 各代说明
  1. 年轻代(Young Generation)
    • 所有新创建的对象都首先进入 Eden 区。
    • 当 Eden 区满了,会触发 Minor GC。
    • 存活对象进入 Survivor 区(两个区轮流使用)。
    • 若对象多次经历 Minor GC 仍未被回收,会被提升(晋升)到老年代。
      回收策略:复制算法(Copying)
  2. 老年代(Old Generation)
    • 存放生命周期较长或大的对象。
    • 空间较大,但回收频率低。
    • 当空间不足时触发 Major GC 或 Full GC,回收代价高,停顿时间长。
    • 回收策略:标记-整理或 G1 的分区整理
  3. 工作原理
    • 对象首先分配在伊甸园区域
    • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1并且交换from和to minor gc会引发stop the world(砸瓦鲁多!!),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
    • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15
      • Java中的对象头中确实分配了一定的字节用来记录对象的年龄,而这个字节的位数是4,因此其二进制最大值为1111,即十进制的15
    • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长