你的对象被回收了吗?

673 阅读5分钟

本文已参与好文召集令活动,点击查看:[后端、大前端双赛道投稿,2万元奖池等你挑战!]

前言

作为一个有经验的开发人员,面试中难免会被问JVM相关的问题,之前我们有讲过对象的分配问题,想回顾的小伙伴可以翻阅往期文章,你真的以为对象都是在堆上分配的么?现在我们再来聊聊对象的回收吧(请有对象的小伙伴看好自己的另一半)。

该被回收了吗

首先,我们需要弄清楚什么样的对象会被回收,总不能正在使用的对象突然就被JVM当作垃圾回收了吧,想必这样的结果广大的程序员都要崩溃了,因此JVM需要准确的判断出谁才是要被回收的垃圾。对于JVM来说,主要判断对象是否需要被回收的方法有两种,引用计数法和可达性分析法,当然,由于引用计数法存在循环引用的情况,所以主流的Java虚拟机中都没用使用它来进行垃圾回收。我们可以看下示例

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;
        //手动gc
        System.gc();
    }
}

当我们执行完main方法时,objA和objB会被回收吗?答案时肯定的,我们看下堆日志的输出结果。首先我们开启IDEA的堆日志打印,Run–>Edit Configuration,在VM Options中设置: -XX:+PrintGCDetails image.png 接着我们运行下代码,查看打印出的日志

D:\tools\jdk\jdk1.8.0_151\bin\java.exe -XX:+PrintGCDetails "-javaagent:D:\IntelliJ IDEA 2020.3.1\lib\idea_rt.jar=63413:D:\IntelliJ IDEA 2020.3.1\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\tianyaa\AppData\Local\Temp\classpath1008509836.jar com.yuan.server.test.ReferenceCountingGC
[GC (System.gc()) [PSYoungGen: 23425K->2302K(74240K)] 23425K->2310K(243712K), 0.0071882 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 2302K->0K(74240K)] [ParOldGen: 8K->2077K(169472K)] 2310K->2077K(243712K), [Metaspace: 3547K->3547K(1056768K)], 0.0098817 secs] [Times: user=0.19 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 74240K, used 427K [0x000000076dc00000, 0x0000000772e80000, 0x00000007c0000000)
  eden space 64000K, 0% used [0x000000076dc00000,0x000000076dc6ac20,0x0000000771a80000)
  from space 10240K, 0% used [0x0000000771a80000,0x0000000771a80000,0x0000000772480000)
  to   space 10240K, 0% used [0x0000000772480000,0x0000000772480000,0x0000000772e80000)
 ParOldGen       total 169472K, used 2077K [0x00000006c9400000, 0x00000006d3980000, 0x000000076dc00000)
  object space 169472K, 1% used [0x00000006c9400000,0x00000006c96076e8,0x00000006d3980000)
 Metaspace       used 3557K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 383K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

可以看到, 23425K->2370K,证明objA和objB并没有因为彼此引用而躲过回收,同时也说明了JVM不是通过引用计数来判断对象是否存活的。

可达性分析算法

当前主流的系统其实都是采用可达性分析算法来判断对象是否存活,这个算法的逻辑大概如下

graph TD
GC_Roots --> object1 --> object2
object1 --> object3
object4 --> object5
object4 --> object6 

可以看到,从GC Roots出发,依次可以经过object1、object2、object3,但是object4、object5、object6虽然有相关联系,但是没有经过GC Roots,所以当发生GC时,还是会被回收掉的。在Java体系中,固定可以作为GC Roots的对象有以下几种:

  • 在虚拟机栈中引用的对象
  • 在方法区中类静态属性引用的对象
  • 在方法区中常量引用的对象
  • 在本地方法栈中JNI引用的对象
  • 所有被同步锁持有的对象

最终死亡

那么经过可达性分析后不可达的对象就一定会被回收吗?答案是否定的,真正宣告一个对象的死亡,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有和GC Roots相连接的引用链,那么它将会被第一次标记,随后进行一次筛选,筛选的条件时此对象是否有必要执行finalize()方法。如果对象没有覆盖finalize()方法,或者该方法已经被虚拟机调用过,那么这时候对象都不会被回收。我们看下在《深入理解Java虚拟机》书中的一个小实验:

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yse,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 Exception {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次自救成功
        SAVE_HOOK = null;
        System.gc();
        //暂停0.5秒等待finalize方法执行
        Thread.sleep(500);
        if(null != SAVE_HOOK){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no,i am dead");
        }

        //对象第二次自救成功,这时候并不会成功
        SAVE_HOOK = null;
        System.gc();
        //暂停0.5秒等待finalize方法执行
        Thread.sleep(500);
        if(null != SAVE_HOOK){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no,i am dead");
        }

    }
}

运行后看下输出:

finalize method executed!
yse,i am still alive
no,i am dead

可以看到,SAVE_HOOK对象的finalize()方法的确被垃圾回收器触发,并且在垃圾回收前成功逃脱过。

总结

以上我们通过一些代码学习了对象回收在不同场景下的不同结果,也知道了对象在什么情况下会被真正回收掉,希望对大家有所帮助。