【JVM】引用计数和可达性分析算法详解

发布于:2023-01-19 ⋅ 阅读:(380) ⋅ 点赞:(0)

前言

JVM堆中几乎存放了所有对象的实例,那么垃圾收集器怎么确定哪些对象还“存活”着,哪些已经“死去”呢?本文主要介绍判断对象是否存活算法引用计数算法和可达性分析算法。


引用计数算法

在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

引用计数算法的缺陷-相互引用代码示例:

/**
 * testGC()方法执行后,objA和objB会不会被GC呢? 
 */
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /*** 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */
    private byte[] bigSize = new byte[8 * _1MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null; 
        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

启动参数设置:

//打印GC信息
-XX:+PrintGCDetails

运行结果:

[GC (System.gc()) [PSYoungGen: 22938K->1183K(76288K)] 22938K->1191K(251392K), 0.0120565 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 1183K->0K(76288K)] [ParOldGen: 8K->1036K(175104K)] 1191K->1036K(251392K), [Metaspace: 3171K->3171K(1056768K)], 0.0178115 secs] [Times: user=0.06 sys=0.02, real=0.02 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b400000, 0x0000000770900000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b400000,0x000000076b5eb9e0,0x000000076f400000)
  from space 10752K, 0% used [0x000000076f400000,0x000000076f400000,0x000000076fe80000)
  to   space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x0000000770900000)
 ParOldGen       total 175104K, used 1036K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
  object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1d031f0,0x00000006cc700000)
 Metaspace       used 3178K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 339K, capacity 392K, committed 512K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:56351', transport: 'socket'

PSYoungGen表示这是一次新生代GC,[PSYoungGen: 22938K->1183K(76288K)] 表示新生代GC情况,22938K->1191K(251392K), 0.0120565 secs表示堆的总体空间情况和耗时,[Times: user=0.08 sys=0.00, real=0.01 secs] user表示用户态CPU耗时,sys表示系统CPU耗时,real表示GC实际经历的时间。

从运行结果中,看出年轻代used 1966K ,意味虚拟机不是通过引用计数算法来判断对象 是否存活的。

可达性分析算法

这个算法的基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

如图下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。

在这里插入图片描述


GC Roots的对象包括以下几种:

·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。

·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

·所有被同步锁(synchronized关键字)持有的对象。

·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

可达性分析回收过程

·如果对象A到GC Roots没有引用链,则进行第一次标记;

进行筛选,判断此对象是否有必要执行finalize()方法 ;


如果对象A没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为"没有必要执行",A被判定为不可触及的;


如果对象A重写了finalize()方法,且还未执行过,那么A会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的finalizer线程触发其finalize()方法执行;

finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记;

如果这期间A在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,A会被移除"即将回收"集合;

如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

自我拯救的代码演示:

/***
 * 此代码演示了两点:
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }

        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

运行结果:

finalize method executed!
yes, i am still alive :)
no, i am dead :(

在这里插入图片描述
点赞 收藏 关注
代码传递思想,技术创造回响。

本文含有隐藏内容,请 开通VIP 后查看