JVM垃圾回收
什么是垃圾回收
垃圾回收指的是:自动清除程序中不再被使用的对象所占用的内存空间。
JVM 会跟踪哪些对象仍然“可达”,哪些对象已经“不可达”,并自动释放不可达对象占用的内存。
内存泄漏:不再使用的对象在系统中未被回收。内存泄漏会导致内存溢出
- 哪些内存区域会被回收?
| 区域 | 是否 GC 管理 |
|---|---|
| Java 堆(Heap) | ✅ 主要回收区 |
| 方法区(元空间) | ✅ 部分可回收(如类元信息) |
| 栈、本地方法栈、程序计数器 | ❌ 不受 GC 管理 |
方法区的回收
🔹二、什么情况下类信息可以被回收?
前提:类必须“不可达”才能被卸载。
JVM 会回收类的元信息,但 只有满足以下条件时,类才会被认为“无用”:
该类的所有实例都被回收;
加载该类的类加载器已被回收;
该类的 java.lang.Class 对象没有被引用。
👉 一句话:类加载器 + 类本身 + 所有实例都“不可达”,才能触发卸载和方法区回收。
使用的情况很少
堆的回收
如何判断对象是否可以回收
可达性分析算法
- 基本思想:
从一组被称为 GC Roots 的根对象出发,沿着对象的引用链向下搜索,凡是能从 GC Roots 直接或间接访问到的对象都是“可达”的”,它们被认为“仍然活着”;而无法访问到的对象是“不可达”的”,就会被判定为垃圾,等待回收。
- GC Roots(根对象)有哪些?
GC Roots 是 JVM 内建的一组引用链起点,它们本身不会被垃圾回收,主要包括:
| 类型 | 说明 |
|---|---|
| 虚拟机栈中的引用变量 | 各个线程正在调用的方法中的局部变量表中的对象引用 |
| 方法区中的静态变量 | 类的静态属性所引用的对象 |
| 方法区中的常量引用 | 字面量引用的对象,如字符串常量池 |
| 本地方法栈中的 JNI 引用 | 本地代码(C/C++)引用的对象 |
| 运行中线程对象本身 | 正在执行的线程默认不会被回收 |
示例
1
2
3
4
5
6
7
8
9
10GC Roots
|
v
[Obj1] ---> [Obj2] ---> [Obj3]
↘
[Obj4]
[Obj5] ←→ [Obj6] ←→ [Obj7]
↑ ↓
GC 无法访问 GC 无法访问- Obj1~4 是可达的对象 → 存活
- Obj5~7 互相引用,但没有任何一个能被 GC Roots 访问 → 不可达 → 可回收
可达性分析的执行步骤
- 从 GC Roots 开始,把所有可直接访问到的对象标记为“可达”;
- 递归遍历这些对象引用的其他对象,也标记为“可达”;
- 最终未被标记的对象即为“不可达”;
- 不可达对象会被 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空间中的对象都可以被视为垃圾,并可以被回收。
- 接下来,详细介绍一下复制算法的工作原理:
- 内存分配:在程序运行过程中,对象的内存分配只在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)分为不同的区域,然后针对不同区域的特点采用不同的垃圾回收算法。
- 各代说明
- 年轻代(Young Generation)
- 所有新创建的对象都首先进入 Eden 区。
- 当 Eden 区满了,会触发 Minor GC。
- 存活对象进入 Survivor 区(两个区轮流使用)。
- 若对象多次经历 Minor GC 仍未被回收,会被提升(晋升)到老年代。
回收策略:复制算法(Copying)
- 老年代(Old Generation)
- 存放生命周期较长或大的对象。
- 空间较大,但回收频率低。
- 当空间不足时触发 Major GC 或 Full GC,回收代价高,停顿时间长。
- 回收策略:标记-整理或 G1 的分区整理
- 工作原理
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1并且交换from和to minor gc会引发stop the world(砸瓦鲁多!!),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15
- Java中的对象头中确实分配了一定的字节用来记录对象的年龄,而这个字节的位数是4,因此其二进制最大值为1111,即十进制的15
- 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长







