微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

JVM 如何标记垃圾对象 之 可达性算法

前情提要,当内存空间不足的时候,JVM 就会触发垃圾回收机制,对垃圾对象进行回收,清理出足够的内存空间,存放新的对象。那么,JVM 是怎么识别垃圾对象的?判断的标准是什么?接下来,让我们一起带着问题,去寻找答案吧!

引用计数法

何为垃圾?没用的、不需要的东西就是垃圾

代码的世界也是如此,当对象 不被引用 的时候,我们就可以认为,这就是一个垃圾对象。那么,有什么办法可以识别哪些对象不被引用呢?

最简单办法,就是对实例对象被引用的次数一个记录,具体实现方式就是在对象头中增加一个计数器属性

◉ 当有一个引用指向实例对象,则引用计数器+1。

◉ 当有一个引用不再指向实例对象,则引用计数器-1。

◉ 当实例对象的引用计数器为0,那么它就是垃圾对象。

这个方法很简单,标记垃圾对象的效率高。

然而,引用计数有一个致命的缺点:循环引用,即实例对象的引用链形成闭环。这种情况,相当于我捉住你,你捉住我,我们彼此都不松开,只能一直维持现状,停留在原地,哪儿都去不了。

这导致实例对象的引用计数器永远都不为 0,意味着实例对象永远都不会被标记垃圾对象,所占有的内存得不到释放,这就产生了 内存泄露。

下面用一段代码,进行例子说明:

// 创建实例A,且 a 引用实例A
// 实例A的引用计数+1 = 1
A a = new A();  
​
// 创建实例B,且 b 引用实例B
// 实例B的引用计数器+1 = 1
B b = new B();  
​
// a.instance 实际上是引用实例B
// 实例B的引用计数器+1 = 2
a.instance = b;  
​
// b.instance 实际上是引用实例A
// 实例A的引用计数器+1 = 2
b.instance = a; 
​
/** 至此,实例A 和 实例B 形成循环引用 **/
​
// 当 a 不再引用实例A
// 实例A的引用计数器-1 = 1
a = null; 
​
// 当 b 不再引用实例B
// 实例B的引用计数器-1 = 1
b = null; 

至此,实例A 和 实例B 的引用计数都不为0,而且,由于两个实例依然存在彼此的引用 且 无法取消引用,那么两个实例的引用计数器都无法归零,因此在采用引用计数法的场景下,两个实例占有的内存都将得不到释放,造成了内存泄漏。

配合两张内存空间的图片,方便大家理解。

图1(实例A 和 实例B 形成循环引用)

图2(a不再指向实例A、b不再指向实例B)

可达性算法

目前主流的虚拟机采用的都是 可达性算法(GC Roots Tracing),这个算法的核心是利用一系列 根对象(GC Roots )作为起始点,根据对象之间的引用关系搜索出一条 引用链(Reference Chain),通过 遍历 引用链来判断对象的是否存活。

如果对象不在任何一条 引用链,即这个对象没有被任何一个 GC Roots 相连,说明这个对象 不可达,那么将会被判定为可回收的垃圾对象。

举个例子,假设 GC Roots 就是迷宫的起点,每个实例对象 就是一块区域,引用链 就是通往各个区域的路线。那些没有任何一条路线可以到达的区域就是可回收的垃圾对象,因为不能到达的区域,并没有任何的意义,只能是浪费空间罢了。

那么,GC Roots 是什么呢?

◉ 虚拟机栈(栈帧的局部变量表)中的引用。

◉ 方法区中类静态属性引用。

◉ 方法区中常量引用。

◉ 本地方法栈JNI(Native方法)引用。

下面分别用几段代码,进行例子说明:

虚拟机栈(栈帧的局部变量表)中的引用

public class Demo {
    public static void main(String[] args) {
        // 创建实例A
        // a1:就是虚拟机栈(栈帧的局部变量表)中引用
        A a1 = new A();
        
        // 当 a1 不再指向实例A的时候,实例A 将不可达
        a1 = null;
    }
}

方法区中类静态属性引用

public class Demo {
    // 创建实例A
    // a2:方法区中类静态属性引用
    public static A a2 = new A();
​
    public static void main(String[] args) {
        // 当 a2 不再指向实例A的时候,实例A 将不可达
        a2 = null;
    }
}

方法区中常量引用

public class Demo {
    // 创建实例A
    // a3:方法区中常量引用
    public static final A a3 = new A();
​
    public static void main(String[] args) {
        // 当 a3 不再指向实例A的时候,实例A 将不可达
        a3 = null;
    }
}

本地方法栈JNI(Native方法)引用

// 访问 java 的构造方法 
JNIEXPORT jobject JNICALL Java_com_test_Demo_accessConstructor
(jnienv * env, jobject jobj) {
    // 通过类的路径从 JVM 找到 A 类
    // a4:本地方法栈JNI(Native方法)引用
    jclass a4 = (*env)->FindClass(env, "java/test/A");
    jmethodID jmid = (*env)->getmethodID(env, jc, "<init>", "()V");
    // 通过 NewObject 实例化,创建实例A
    jobject date_obj = (*env)->NewObject(env, jc,jmid);
    return date_obj;
}

配合一张内存空间的图片,方便大家理解。

图3

再说一句

以上的内容,简单讲述了 JVM 标记垃圾对象的两个方法:引用计数法 和 可达性算法。其中,引用计数法 我们只需要简单了解,可达性算法 才是目前主流虚拟机所采用的算法。

然而,可达性算法 在真正实现的场景下,有什么值得深入了解的地方呢?例如:如何枚举 GC Roots?GC Roots 向下遍历的优化?并发场景下标记对象会有存在什么问题?学无止境,这里挖一个坑,后面将会带大家深入了解这些问题,尽情期待。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐