【JVM】根可达算法
根可达性算法可以理解为一句话:
从一批“绝对不能被回收的对象”出发,顺着引用关系往外找,能找到的对象就是存活对象,找不到的就是垃圾对象。
一、什么是 GC Root?
GC Root 就是垃圾回收时的“起点对象”。
JVM 在判断对象是否存活时,不是看这个对象有没有被别人引用,而是看:
这个对象能不能从 GC Root 出发,通过引用链找到。
比如:
A->B->C如果A是 GC Root,那么 JVM 从A出发,可以找到B,再找到C。
所以:
A、B、C 都是存活对象但是如果有:
D->EE->D虽然D和E互相引用,但如果从任何 GC Root 都找不到它们,那么它们就是垃圾。
这也解决了引用计数法无法解决的循环引用问题。
二、根可达性算法的判断过程
可以分成三步:
1. 找出 GC Root
JVM 先找到一批根对象,比如:
线程栈里的局部变量 静态变量 常量引用 JNI 本地方法引用 被 synchronized 加锁的对象这些对象被认为是程序当前执行过程中“还可能用到的对象”。
2. 从 GC Root 出发沿引用链查找
例如:
publicclassDemo{staticObjectstaticObj=newObject();publicstaticvoidmain(String[]args){ObjectlocalObj=newObject();ObjectinnerObj=newObject();localObj=innerObj;}}这里可能形成这样的引用关系:
GC Root | |-- staticObj | |-- main线程栈中的 localObj只要对象能从这些 GC Root 找到,就不会被回收。
3. 找不到的对象就是垃圾
例如:
Objecta=newObject();Objectb=newObject();a=null;b=null;当a和b都不再引用原来的对象后,如果这些对象也没有被其他 GC Root 间接引用,那么它们就变成垃圾对象。
三、哪些对象可以作为 GC Root?
你写的四类可以这么理解。
1. 系统类的实例对象
这个说法可以理解为:
由系统类加载器加载的一些核心类对象,或者方法区中类静态属性引用的对象,可以作为 GC Root。
更常见的面试说法是:
方法区中类静态属性引用的对象。
比如:
publicclassUserService{publicstaticObjectobj=newObject();}这里的obj是静态变量,属于类级别的变量。
只要UserService这个类还被加载着,那么:
UserService.class -> static obj -> new Object()这个new Object()就可以从 GC Root 找到,所以不会被回收。
可以简单记成:
静态变量引用的对象,可以作为 GC Root 直接或间接可达的对象2. 本地方法调用相关的实例对象
也就是:
本地方法栈中 JNI 引用的对象。
Java 有时会调用 Native 方法,也就是用 C/C++ 写的方法,比如:
publicnativevoidmethod();如果 Native 方法中持有了某个 Java 对象的引用,那么 JVM 不能随便回收这个对象。
因为 Native 代码可能还在使用它。
所以这类对象也可以作为 GC Root。
可以简单理解为:
被 native 方法引用的 Java 对象,不能被回收3. 当前活动线程相关的实例对象
这是最常见、最重要的一类。
准确说是:
虚拟机栈中栈帧里的局部变量表引用的对象,可以作为 GC Root。
例如:
publicvoidtest(){Objectobj=newObject();}当test()方法正在执行时,obj是局部变量,存放在当前线程的栈帧中。
此时:
当前线程栈帧中的 obj -> new Object()所以这个对象不能被回收。
再比如:
publicvoidtest(){Useruser=newUser();user.setName("Tom");}只要test()方法还没执行结束,user引用的对象通常就是可达的。
可以简单记成:
正在运行的方法中的局部变量引用的对象,不能回收4. 被加锁的实例对象
也就是:
被 synchronized 持有的对象,可以作为 GC Root。
例如:
Objectlock=newObject();synchronized(lock){// 临界区代码}当线程进入synchronized(lock)代码块后,lock对象正在作为锁使用。
这个对象不能被回收,否则锁的语义就出问题了。
所以 JVM 会认为被锁持有的对象是可达的。
可以简单理解为:
正在被 synchronized 当锁用的对象,不能被回收四、举个完整例子
publicclassDemo{staticObjectstaticObj=newObject();publicstaticvoidmain(String[]args){ObjectlocalObj=newObject();ObjectuselessObj=newObject();uselessObj=null;}}在main方法执行期间:
GC Roots | |-- staticObj -> Object1 | |-- main线程栈中的 localObj -> Object2所以:
Object1 不会被回收 Object2 不会被回收而:
ObjectuselessObj=newObject();uselessObj=null;原来被uselessObj引用的对象已经找不到了。
所以:
Object3 可能被回收注意是可能被回收,不是立刻回收。
因为 GC 什么时候发生,由 JVM 决定。
五、面试回答版本
你可以这样说:
Java 判断对象是否为垃圾,主要使用根可达性算法。JVM 会从一组 GC Roots 对象出发,沿着对象之间的引用链向下搜索。如果某个对象能够从 GC Roots 到达,说明它仍然被使用,不能回收;如果某个对象无法从任何 GC Roots 到达,那么它就可以被判定为垃圾对象。常见的 GC Roots 包括:虚拟机栈中局部变量引用的对象、方法区中静态变量或常量引用的对象、本地方法栈中 JNI 引用的对象,以及被 synchronized 锁持有的对象等。
更口语一点:
简单来说,就是从线程栈、静态变量、常量、Native 方法引用、锁对象这些根对象开始找,能找到的对象就是活的,找不到的对象就是垃圾。
你这四类可以稍微改得更标准一些:
1. 虚拟机栈中局部变量表引用的对象 2. 方法区中类静态属性引用的对象 3. 方法区中常量引用的对象 4. 本地方法栈中 JNI 引用的对象 5. 被 synchronized 持有的对象其中你写的:
当前活动线程相关的实例对象对应的是虚拟机栈中的局部变量引用对象。
你写的:
系统类的实例对象更标准地说应该是方法区中静态变量或常量引用的对象。
