ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2021-09-19 22:05:38  阅读:170  来源: 互联网

标签:对象 计数器 public 算法 实例 引用 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 向下遍历的优化?并发场景下标记对象会有存在什么问题?学无止境,这里挖一个坑,后面将会带大家深入了解这些问题,尽情期待。

标签:对象,计数器,public,算法,实例,引用,JVM,可达性,垃圾
来源: https://blog.csdn.net/zero_97/article/details/120385262

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有