【JVM】根可达算法

根可达性算法可以理解为一句话:

从一批"绝对不能被回收的对象"出发,顺着引用关系往外找,能找到的对象就是存活对象,找不到的就是垃圾对象。


一、什么是 GC Root?

GC Root 就是垃圾回收时的"起点对象"。

JVM 在判断对象是否存活时,不是看这个对象有没有被别人引用,而是看:

这个对象能不能从 GC Root 出发,通过引用链找到。

比如:

java 复制代码
A -> B -> C

如果 A 是 GC Root,那么 JVM 从 A 出发,可以找到 B,再找到 C

所以:

text 复制代码
A、B、C 都是存活对象

但是如果有:

java 复制代码
D -> E
E -> D

虽然 DE 互相引用,但如果从任何 GC Root 都找不到它们,那么它们就是垃圾。

这也解决了引用计数法无法解决的循环引用问题


二、根可达性算法的判断过程

可以分成三步:

1. 找出 GC Root

JVM 先找到一批根对象,比如:

text 复制代码
线程栈里的局部变量
静态变量
常量引用
JNI 本地方法引用
被 synchronized 加锁的对象

这些对象被认为是程序当前执行过程中"还可能用到的对象"。


2. 从 GC Root 出发沿引用链查找

例如:

java 复制代码
public class Demo {
    static Object staticObj = new Object();

    public static void main(String[] args) {
        Object localObj = new Object();
        Object innerObj = new Object();

        localObj = innerObj;
    }
}

这里可能形成这样的引用关系:

text 复制代码
GC Root
  |
  |-- staticObj
  |
  |-- main线程栈中的 localObj

只要对象能从这些 GC Root 找到,就不会被回收。


3. 找不到的对象就是垃圾

例如:

java 复制代码
Object a = new Object();
Object b = new Object();

a = null;
b = null;

ab 都不再引用原来的对象后,如果这些对象也没有被其他 GC Root 间接引用,那么它们就变成垃圾对象。


三、哪些对象可以作为 GC Root?

你写的四类可以这么理解。


1. 系统类的实例对象

这个说法可以理解为:

由系统类加载器加载的一些核心类对象,或者方法区中类静态属性引用的对象,可以作为 GC Root。

更常见的面试说法是:

方法区中类静态属性引用的对象。

比如:

java 复制代码
public class UserService {
    public static Object obj = new Object();
}

这里的 obj 是静态变量,属于类级别的变量。

只要 UserService 这个类还被加载着,那么:

text 复制代码
UserService.class -> static obj -> new Object()

这个 new Object() 就可以从 GC Root 找到,所以不会被回收。

可以简单记成:

text 复制代码
静态变量引用的对象,可以作为 GC Root 直接或间接可达的对象

2. 本地方法调用相关的实例对象

也就是:

本地方法栈中 JNI 引用的对象。

Java 有时会调用 Native 方法,也就是用 C/C++ 写的方法,比如:

java 复制代码
public native void method();

如果 Native 方法中持有了某个 Java 对象的引用,那么 JVM 不能随便回收这个对象。

因为 Native 代码可能还在使用它。

所以这类对象也可以作为 GC Root。

可以简单理解为:

text 复制代码
被 native 方法引用的 Java 对象,不能被回收

3. 当前活动线程相关的实例对象

这是最常见、最重要的一类。

准确说是:

虚拟机栈中栈帧里的局部变量表引用的对象,可以作为 GC Root。

例如:

java 复制代码
public void test() {
    Object obj = new Object();
}

test() 方法正在执行时,obj 是局部变量,存放在当前线程的栈帧中。

此时:

text 复制代码
当前线程栈帧中的 obj -> new Object()

所以这个对象不能被回收。

再比如:

java 复制代码
public void test() {
    User user = new User();
    user.setName("Tom");
}

只要 test() 方法还没执行结束,user 引用的对象通常就是可达的。

可以简单记成:

text 复制代码
正在运行的方法中的局部变量引用的对象,不能回收

4. 被加锁的实例对象

也就是:

被 synchronized 持有的对象,可以作为 GC Root。

例如:

java 复制代码
Object lock = new Object();

synchronized (lock) {
    // 临界区代码
}

当线程进入 synchronized(lock) 代码块后,lock 对象正在作为锁使用。

这个对象不能被回收,否则锁的语义就出问题了。

所以 JVM 会认为被锁持有的对象是可达的。

可以简单理解为:

text 复制代码
正在被 synchronized 当锁用的对象,不能被回收

四、举个完整例子

java 复制代码
public class Demo {
    static Object staticObj = new Object();

    public static void main(String[] args) {
        Object localObj = new Object();
        Object uselessObj = new Object();

        uselessObj = null;
    }
}

main 方法执行期间:

text 复制代码
GC Roots
  |
  |-- staticObj -> Object1
  |
  |-- main线程栈中的 localObj -> Object2

所以:

text 复制代码
Object1 不会被回收
Object2 不会被回收

而:

java 复制代码
Object uselessObj = new Object();
uselessObj = null;

原来被 uselessObj 引用的对象已经找不到了。

所以:

text 复制代码
Object3 可能被回收

注意是可能被回收,不是立刻回收。

因为 GC 什么时候发生,由 JVM 决定。


五、面试回答版本

你可以这样说:

Java 判断对象是否为垃圾,主要使用根可达性算法。JVM 会从一组 GC Roots 对象出发,沿着对象之间的引用链向下搜索。如果某个对象能够从 GC Roots 到达,说明它仍然被使用,不能回收;如果某个对象无法从任何 GC Roots 到达,那么它就可以被判定为垃圾对象。常见的 GC Roots 包括:虚拟机栈中局部变量引用的对象、方法区中静态变量或常量引用的对象、本地方法栈中 JNI 引用的对象,以及被 synchronized 锁持有的对象等。

更口语一点:

简单来说,就是从线程栈、静态变量、常量、Native 方法引用、锁对象这些根对象开始找,能找到的对象就是活的,找不到的对象就是垃圾。


你这四类可以稍微改得更标准一些:

text 复制代码
1. 虚拟机栈中局部变量表引用的对象
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中 JNI 引用的对象
5. 被 synchronized 持有的对象

其中你写的:

text 复制代码
当前活动线程相关的实例对象

对应的是 虚拟机栈中的局部变量引用对象

你写的:

text 复制代码
系统类的实例对象

更标准地说应该是 方法区中静态变量或常量引用的对象

相关推荐
乐迪信息1 小时前
乐迪信息:AI算法盒子实时识别船舶烟雾与火焰异常
大数据·人工智能·算法·安全·目标跟踪
艾iYYY1 小时前
string 类的模拟实现
android·服务器·c语言·c++·算法
Lsk_Smion2 小时前
力扣实训 _ [75].颜色分类 _ 杨辉三角
数据结构·算法·leetcode
jidaowansui2 小时前
P11375 [GESP202412 六级] 树上游走
数据结构·算法
hai3152475433 小时前
FlashAttention C语言(C++)实现(展示版)
c语言·开发语言·c++·人工智能·算法
林爷万福3 小时前
光谱数据预处理:基线校正、平滑去噪实战
人工智能·算法
8Qi84 小时前
LeetCode 1049:最后一块石头的重量 II —— 题解 ✅
算法·leetcode·职场和发展·动态规划·01背包
wuminyu4 小时前
Java锁机制之Java对象重量级锁源码剖析
java·linux·c语言·jvm·c++