一、核心问题:JVM 怎么判断 "对象死了"?(垃圾判定算法)
堆里的对象成千上万,GC 首先要解决 "哪些是垃圾(不能再用)" 的问题,主流有两种算法,重点是可达性分析算法(现在所有 JVM 都在用)。
1. 引用计数法:简单但有致命缺陷
核心逻辑:
给每个对象装一个 "计数器",记录当前有多少地方在引用它:
- 有人引用它(比如
A a = new A()),计数器 + 1; - 引用失效(比如
a = null),计数器 - 1; - 计数器 = 0 → 判定为垃圾,等待回收。
优点:
实现超级简单,计算速度快(不用遍历整个引用链)。
致命缺陷:解决不了 "循环引用"
这就是它被淘汰的原因!比如你给的代码:
java
ReferenceCountingGc objA = new ReferenceCountingGc(); // objA引用objA对象,计数器=1
ReferenceCountingGc objB = new ReferenceCountingGc(); // objB引用objB对象,计数器=1
objA.instance = objB; // objA对象引用objB对象,objB计数器+1 → 2
objB.instance = objA; // objB对象引用objA对象,objA计数器+1 → 2
objA = null; // 原objA变量的引用失效,objA对象计数器-1 → 1
objB = null; // 原objB变量的引用失效,objB对象计数器-1 → 1
此时,objA和objB对象已经没有任何 "外部引用"(程序再也用不到它们了),但因为互相引用,计数器都是 1,引用计数法会误以为它们 "还在被使用",永远不会回收 ------ 这会导致内存泄漏(没用的对象占着内存不放)。
2. 可达性分析算法:JVM 的 "标准答案"
核心逻辑:
用 "引用链追踪" 代替计数器,核心是 "从根节点出发,能找到的就是活的,找不到的就是死的":
- 确定一批 "GC Roots"(根对象)------ 这些对象是 "绝对不可能被回收" 的(相当于仓库的 "承重墙",不能动);
- 从 GC Roots 出发,像 "寻宝" 一样向下遍历所有被它们引用的对象,形成一条 "引用链";
- 能被遍历到的对象(有引用链连接)→ 活着;遍历不到的对象(断了引用链)→ 垃圾。
关键:哪些是 GC Roots?(必记)
你可以理解为 "JVM 认定的、不会被回收的核心对象",主要包括:
- 线程栈的本地变量:比如方法里的局部变量(
main方法里的objA、objB)、方法参数; - 静态变量:类级别的变量(
public static User user),属于类,只要类还在,它就活着; - 本地方法栈的变量:调用 native 方法(比如 Java 调用 C/C++ 代码)时用到的变量;
- 活跃线程对象:正在运行的线程本身(线程没结束,它的相关资源不能回收);
- 方法区里的常量:比如
String常量池里的字符串("abc")。
解决循环引用问题:
回到之前的循环引用案例:
- 当
objA = null和objB = null后,objA和objB对象不再被任何 GC Roots 引用(它们的引用链断了); - 即使它们互相引用,可达性分析算法也会判定它们是 "不可达" 的 → 标记为垃圾,后续会被回收。
这就是为什么可达性分析算法能成为主流 ------ 彻底解决了循环引用的坑。
二、补充:Java 的 4 种引用类型(影响对象 "存活优先级")
可达性分析里的 "引用" 不是 "非有即无" 的,Java 把引用分成 4 种强度,强度从高到低,决定了对象被 GC 回收的 "难易程度"------ 这是对 "垃圾判定" 的补充:不同引用类型的对象,GC 对待它们的策略不一样。
| 引用类型 | 核心特点 | 实际用途 | 示例代码 |
|---|---|---|---|
| 强引用 | 最普通的引用(我们平时写的),GC 绝对不回收 | 绝大多数业务对象(比如User、List) |
User user = new User(); |
| 软引用 | 内存足够时不回收,内存不够(要 OOM)时才回收 | 内存敏感的缓存(比如浏览器缓存、图片缓存) | SoftReference<User> user = new SoftReference<>(new User()); |
| 弱引用 | 不管内存够不够,GC 一触发就回收 | 偶尔用的缓存(比如临时数据,丢了也没关系) | WeakReference<User> user = new WeakReference<>(new User()); |
| 虚引用 | 最弱的引用,几乎等同于没有引用,不能通过它访问对象 | 唯一用途:跟踪对象被 GC 回收的时刻(比如记录日志) | PhantomReference<User> user = new PhantomReference<>(new User(), 引用队列); |
通俗解释:
- 强引用:"刚需房"------ 只要有人住(有引用),绝对不拆;
- 软引用:"廉租房"------ 内存够就住,内存紧张时就被清退;
- 弱引用:"日租房"------ 当天(GC 触发)就清退,不管有没有空位;
- 虚引用:"幽灵房"------ 房子已经快拆了,只能远远看着(跟踪回收),不能住。
关键应用:软引用实现缓存
比如浏览器后退功能:
- 浏览过的网页用软引用缓存起来(
SoftReference<WebPage>); - 内存足够时,后退能直接从缓存取,不用重新请求;
- 内存不够时,GC 会回收这些缓存页面,避免 OOM------ 既兼顾了性能,又保证了内存安全。
三、对象 "死刑复核":finalize () 方法的最后机会
可达性分析判定为 "不可达" 的对象,不是立刻就被回收,而是进入 "缓刑期"------ 要经历两次标记,finalize () 是唯一的 "减刑机会"。
核心流程:
-
第一次标记:
- 可达性分析后,对象不可达 → 第一次标记;
- 筛选:判断对象是否 "有必要执行 finalize () 方法";
- 没覆盖 finalize () 方法 → 直接判定为 "死刑",等待回收;
- 覆盖了 finalize () 方法 → 暂时不回收,把对象放进一个叫 "F-Queue" 的队列,等待后续处理。
-
第二次标记:
- JVM 会启动一个低优先级的线程,去执行 F-Queue 里对象的 finalize () 方法(注意:不一定能执行完,因为线程优先级低,可能被打断);
- 这是对象 "自救" 的最后机会:如果在 finalize () 里,对象重新和引用链建立关联(比如把自己赋值给某个静态变量),那么第二次标记时,它会被移出 "待回收集合",继续活着;
- 如果没自救,或者 finalize () 执行完还没关联 → 第二次标记后,正式判定为 "死刑",后续 GC 会回收它。
关键注意点:
- finalize () 只能执行一次:一个对象的 finalize () 被 JVM 调用后,就算它再次被判定为不可达,JVM 也不会再执行它的 finalize ()------ 相当于 "上诉只有一次机会";
- 不推荐用 finalize ():它的执行时机不确定(可能很久才执行),而且容易写错(比如没真正自救,还占着资源),现在基本被
try-with-resources、CleanerAPI 替代。
自救示例(理解即可,不要写):
java
public class FinalizeTest {
public static FinalizeTest saveMe; // 静态变量,属于GC Roots
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("执行finalize()方法");
saveMe = this; // 自救:把当前对象赋值给静态变量,重新关联引用链
}
public static void main(String[] args) throws InterruptedException {
saveMe = new FinalizeTest();
// 第一次让对象不可达
saveMe = null;
System.gc(); // 触发GC
Thread.sleep(500); // 等待低优先级线程执行finalize()
if (saveMe != null) {
System.out.println("对象自救成功"); // 会打印,因为finalize()里重新关联了
} else {
System.out.println("对象已被回收");
}
// 第二次让对象不可达
saveMe = null;
System.gc();
Thread.sleep(500);
if (saveMe != null) {
System.out.println("对象自救成功");
} else {
System.out.println("对象已被回收"); // 会打印,因为finalize()只能执行一次
}
}
}
四、扩展:方法区的 "垃圾回收"------ 无用类的判定
我们之前聊的都是 "堆里的对象回收",但方法区(元空间)也会产生垃圾(比如无用的类),只是回收频率很低(因为类的生命周期通常很长)。
核心:什么是 "无用的类"?(3 个条件必须同时满足)
类不像对象那样 "容易死",要被回收必须满足以下 3 个条件(相当于 "类的棺材钉" 要钉 3 颗):
- 堆中没有该类的任何实例:比如
User类,所有new User()创建的对象都被回收了,堆里找不到User实例; - 加载该类的 ClassLoader 已经被回收:比如自定义的类加载器(不是 JVM 自带的 BootstrapClassLoader),它本身被回收了(没有引用指向它);
- 该类的
Class对象没有被引用:比如不能通过反射访问它(Class.forName("com.test.User")都获取不到),也没有任何地方持有这个Class对象的引用。
注意:
- 满足这 3 个条件,JVM可以选择回收这个类,不是 "必须回收"(比如 JVM 为了性能,可能暂时不回收);
- 常见场景:动态生成类的框架(比如 Spring、MyBatis),会动态创建很多类,如果不及时回收,可能导致方法区溢出(OOM: Metaspace)。
五、总结:把所有知识点串起来
JVM 的垃圾回收流程可以简化为:
- 用可达性分析算法(基于 GC Roots)判断对象是否可达 → 不可达的对象进入 "缓刑期";
- 结合4 种引用类型调整回收优先级(强引用不回收,软引用内存不够才回收,弱引用 GC 就回收);
- 对 "缓刑期" 对象进行两次标记,通过finalize() 给予最后自救机会 → 最终判定为垃圾;
- 堆里的垃圾对象被回收,方法区里满足 3 个条件的无用类也可能被回收。
核心记住 3 个关键点:
- 垃圾判定的核心是 "可达性分析",不是 "引用计数";
- GC Roots 是 "不可回收的根对象",包括本地变量、静态变量等;
- 引用类型决定回收优先级,finalize () 是最后自救机会(但不推荐用)。