
JVM — Garbage Collection
一、如何判断对象可以回收
1、引用计数法(Reference Counting)
引用计数法是最简单的一种内存回收判断方式。其基本原理是:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;当引用计数为0时,说明该对象不再被任何变量引用,可以被回收。
缺点:无法处理循环引用,即两个对象互相引用,即使它们不再被程序访问,但计数始终不为0,无法回收。
如下图,即使两个对象没有被使用,但两个对象的引用计数始终为1。

2、可达性分析算法(Reachability Analysis)
可达性分析法,是主流 JVM(如 HotSpot)采用的方式。其基本思想是:通过一系列称为“GC Roots”的对象(肯定不能作为垃圾被回收的对象)作为起点,扫描堆中所有的对象,凡是被 GC Roots 直接或间接引用的对象,都称为可达对象,还在被使用;不可达的对象则视为垃圾,可以回收。
扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收。
常见的 GC Roots 包括:
"栈静常锁线程活,JNI全局JVM持"
其中,"栈"指虚拟机栈中的引用对象,"静"指静态变量引用,"常"指常量引用,"锁"指同步锁引用,"线程活"指活跃线程对象,"JNI全局"指JNI全局引用,"JVM保"指JVM内部持有的对象。
(1) 虚拟机栈(JVM Stack)中的局部变量
每个线程都有自己的虚拟机栈,栈帧中存储了方法调用时的局部变量表(如方法参数、临时变量)。这些局部变量直接引用的对象是 GC Root。
public void method() {
Object obj = new Object(); // obj 是 GC Root
}(2) 方法区(Method Area)中的静态变量
静态变量(static 修饰的字段)属于类级别,其生命周期与 JVM 相同。静态变量引用的对象是 GC Root。
public class MyClass {
private static Object staticObj = new Object(); // staticObj 是 GC Root
}(3) 本地方法栈(Native Method Stack)中的 JNI 引用
通过 JNI(Java Native Interface)调用的本地方法(C/C++ 实现)中引用的对象。本地方法栈中的引用也会被 JVM 识别为 GC Root。
(4) 活跃线程(Active Threads)
正在运行的线程对象(如主线程、用户创建的线程)及其栈中的局部变量。线程本身是 GC Root,且线程栈中的引用对象也是 GC Root。
(5) 方法区中的常量引用
方法区中的 String 常量池或类常量池中引用的对象。例如:字符串字面量(如 "hello")是 GC Root。
(6) 被 JVM 持有的对象
JVM 内部管理的对象,如:
Class对象(类元数据)Method、Field等反射相关的对象- 一些 JVM 内部使用的特殊对象
(7) 其他系统级引用
例如:系统类加载器(Bootstrap/Extension/Application ClassLoader)加载的类对象。JVM 内部维护的全局缓存或监控对象。
3、四种引用类型(强 软 弱 虚)
(1) 强引用(Strong Reference)
定义:最常见的引用类型,如 Object obj = new Object();。
特点:① 只要对象有强引用存在,垃圾回收器 永远不会回收该对象。② 如果内存不足且无法回收强引用对象,会抛出 OutOfMemoryError。
String str = new String("Hello"); // 强引用(2) 软引用(Soft Reference)
定义:通过 SoftReference<T> 类实现。
特点:对象仅被软引用关联时,在内存不足时才会被回收。
适用场景:缓存数据(如图片、文件等),内存不足时自动清理。
String str = new String("Hello");
SoftReference<String> softRef = new SoftReference<>(str);
str = null; // 移除强引用
// 当 JVM 内存不足时,softRef.get() 可能返回 null(3) 弱引用(Weak Reference)
定义:通过 WeakReference<T> 类实现。
特点:对象仅被弱引用关联时,无论内存是否足够,下一次 GC 都会回收该对象。
适用场景:避免内存泄漏(如监听器列表、缓存池)。
String str = new String("Hello");
WeakReference<String> weakRef = new WeakReference<>(str);
str = null;
// 下一次 GC 后,weakRef.get() 会返回 null(4) 虚引用(Phantom Reference)
定义:通过 PhantomReference<T> 类实现,必须配合 ReferenceQueue 使用。
特点:虚引用无法通过 get() 方法获取对象(始终返回 null)。对象被回收后,虚引用会被放入 ReferenceQueue,用于 跟踪对象被回收的状态。
适用场景:监控对象被回收的时机(如释放本地资源、堆外内存)。
String str = new String("Hello");
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>(str, queue);
str = null;
// 通过 ReferenceQueue 获取被回收的对象
Reference<? extends String> ref = queue.poll();
if (ref != null) {
System.out.println("对象已被回收");
}四种引用类型的对比表
| 引用类型 | 是否阻止 GC | 回收时机 | 典型用途 |
|---|---|---|---|
| 强引用 | ✅ | 永远不回收 | 核心业务对象 |
| 软引用 | ❌ | 内存不足时回收 | 缓存(如图片缓存) |
| 弱引用 | ❌ | 下一次 GC 必定回收 | 临时缓存、监听器 |
| 虚引用 | ❌ | 对象被回收后加入 ReferenceQueue | 资源释放、堆外内存管理 |
二、垃圾回收算法
1、标记-清除算法(Mark Sweep)

所谓的清除操作,只需要把占用内存的起始地址保存下来,记录为空闲空间,以便复用。
优点:速度快
缺点:产生内存碎片(空间不连续)
2、标记-整理算法(Mark Compact)

优点:没有空间碎片
缺点:速度较慢
3、复制算法(Copy)
(1) 标记垃圾

(2) 复制存活对象到To区,清除From区

(3) 交换From和To

三、分代垃圾回收
1、相关VM参数
| 含义 | 参数 |
|---|---|
| 堆初始大小 | -Xms |
| 堆最大大小 | -Xmx 等价于 -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn 直接指定新生代大小,设置大小范围 ↓ |
| -XX:NewSize=size -XX:MaxNewSize=size | |
| 幸存区动态比例 | -XX:InitialSurvivorRatio=ratio 设置初始+↓ |
| -XX:+UseAdaptiveSizePolicy | |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 默认为8,指伊甸园,剩下2平分给From和To | |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribution |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC前 MinorGC | -XX:+ScavengeBeforeFullGC |
2、分代垃圾回收过程
为什么要做分代划分呢?
因为 Java 中有的对象需要长时间引用,这种对象就适合放在老年代中。而用完了就可以丢的对象,就放在新生代中。这样就可以根据对象生命周期的不同特点,进行不同的垃圾回收策略。老年代的垃圾回收很久才发生一次,新生代的垃圾回收会频繁一些。
(1) 新生的对象会放到伊甸园区
如果伊甸园区满了,放不下新的对象了,会触发一次 Minor GC (第一次垃圾回收)

Minor GC 会沿着 GC Root 扫描伊甸园区,进行一次垃圾标记操作。然后采用复制算法,将存活的对象复制到 幸存区To 中,并将幸存对象的寿命+1

复制完成后,交换From和To:

(2) Minor GC 后,过了一段时间,伊甸园区又满了
再次触发 Minor GC(第二次垃圾回收)
扫描沿着 GC Root 扫描新生代内存(伊甸园区和From区),进行垃圾标记,将幸存对象复制到To区,幸存对象寿命+1,清除From区,然后交换From和To区。

步骤总结:新生对象都会放在伊甸园区,当伊甸园满了,就会触发 Minor GC,将伊甸园区和From区的幸存对象,复制到To区,幸存对象寿命+1,清除被标记为垃圾的对象,然后交换From区和To区。这种复制-清除(复制幸存对象到To区-清除From区)和交换两个区的操作,使得每次 Minor GC 完成后,From区总是本次GC幸存下来的对象,To区总是空的,为下一次GC待命。
Minor GC 会引发 stop the world(用户线程都要暂停,垃圾回收线程运行,直到垃圾回收结束),因为对象的复制操作,和区域交换操作,对象的地址会发生改变。不过 Minor GC 暂停时间比较短,因为新生代垃圾较多,需要复制的幸存对象并不多。
(3) 幸存区对象晋升到老年代
幸存对象不会永远在幸存区待着,当他们的寿命超过一定的阈值(最大是15次)时,即可晋升到老年代。对象的寿命存在对象头中,是4bit,最大值是15(1111)。

(4) 新生代和老年代几乎全满了,触发 Full GC
一般垃圾回收操作,都会在空间不足时触发
当老年代的空间不足时,会先尝试触发 Minor GC,如果空间还是不够,会触发 Full GC ,进行一次新生代和老年代的全面清理。Full GC 导致的 STW 时间更长。
老师的总结:
四、垃圾回收器

