引用计数与可达性分析

本文介绍了Java中的垃圾回收机制,重点讨论了引用计数算法和可达性分析两种对象存活判断方式。引用计数算法虽然简单高效,但无法处理循环引用问题,而Java采用可达性分析避免这一问题,通过GC Roots确定对象的可达性。同时,文章提到了finalization机制,强调不应手动调用对象的finalize()方法,该方法在适当时候由垃圾回收器调用,影响对象的生命周期状态。

一 GC的一些基本情况:

  1. GC收集情况:频繁收集年轻代,较少回收老年代,基本不动永久代(元空间),由此可以看到,在GC中,大多数是对堆空间的回收
  2. 那么在java中,是如何判断一个对象是否是个垃圾呢? 简单来说:当一个对已经不再被任何存活对象继续引用时,就可以宣布为死亡

二 判断对象是否存活的方式:

1.引用计数算法:引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。

例如:有对象objA,与objB,如果objA指向了objB,那么objB中的引用计数器就会+1。当objA对objB的引用失效时,objB的引用计数器就-1.当objB的引用计数器的值为0时,则可以被回收。

附上代码:
package com.cdc.gc.demo;

/**
 * @author cdc
 * @email c925638766@163.com
 * @date 2020/7/29 17:38
 * 测试在java中是否用了引用计数算法标记垃圾
 *  -XX:+PrintGCDetails
 *
 *
 */
public class RFCount {
    private byte[] buff=new byte[1024*1024*5];//5MB

    Object reference=null;//引用

    public static void main(String[] args) {
        RFCount rfCount1=new RFCount();
        RFCount rfCount2=new RFCount();

        rfCount1.reference=rfCount2;//将对象1的属性指向对象2,构成引用
        rfCount2.reference=rfCount1;//将对象2的属性指向对象1,构成引用

        //解除引用
        rfCount1=null;
        rfCount2=null;

        //System.gc();
    }
}

在这里,首先通过rfCount1指向了堆中的RFCount(),假设该地址为0x1122,rfCount2指向的RFCount()地址假设为0x2233。这时0x1122与0x2233中的引用计数器都为1。之后在通过rfCount1中的reference指向rfCount2,即0x2233的地址再次被指向,引用计数器+1,即为2;同理0x2233的引用计数器也+1。之后再交rfCount1与rfCount2分别=null,即断开引用,引用失效,那么0x1122与0x2233的计数器分别-1,即为1。此时通过-XX:+PrintGCDetails参数,打印GC详细信息。

当System.gc()被注释起来后,即没有显式的调用gc时,控制台打印的信息:
"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" -XX:+PrintGCDetails "-javaagent:F:\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=49753:F:\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;F:\Java自学\jvm\JVMDemo\out\production\JVMDemo;F:\Maven_jars\junit\junit\4.11\junit-4.11.jar;F:\Maven_jars\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar" com.cdc.gc.demo.RFCount
Heap
 PSYoungGen      total 75776K, used 16742K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 25% used [0x000000076b700000,0x000000076c759bd0,0x000000076f680000)
  from space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
  to   space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
 ParOldGen       total 173568K, used 0K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 0% used [0x00000006c2400000,0x00000006c2400000,0x00000006ccd80000)
 Metaspace       used 3196K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

在这里可以看到,年轻代中的总内存是75776k,使用的是16742k,老年代使用为0k。

当System.gc()没有注释起来后,即显式的调用gc时,控制台打印的信息:
"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" -XX:+PrintGCDetails "-javaagent:F:\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=49963:F:\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;F:\Java自学\jvm\JVMDemo\out\production\JVMDemo;F:\Maven_jars\junit\junit\4.11\junit-4.11.jar;F:\Maven_jars\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar" com.cdc.gc.demo.RFCount
[GC (System.gc()) [PSYoungGen: 15442K->696K(75776K)] 15442K->704K(249344K), 0.0018542 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 696K->0K(75776K)] [ParOldGen: 8K->626K(173568K)] 704K->626K(249344K), [Metaspace: 3132K->3132K(1056768K)], 0.0075210 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 1951K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 3% used [0x000000076b700000,0x000000076b8e7c68,0x000000076f680000)
  from space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
  to   space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
 ParOldGen       total 173568K, used 626K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 0% used [0x00000006c2400000,0x00000006c249c9a0,0x00000006ccd80000)
 Metaspace       used 3180K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 344K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

可以看到gc的发生,并且年轻代中的已用空间为1951k,可以判断出,堆中的0x1122与0x2233都被回收了。

结论:
1.在java中,并没有使用引用计数算法来进行垃圾回收。
2.引用计数器的优点:实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟性
3.引用计数器的缺点:
①.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销;
②.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销;
③.引用计数器有一个严重的问题,即无法处理循环引用的情况。这也是java没有采用引用计数算法的原因。但是在python中,使用了引用计数算法
循环引用:

2.可达性分析(根搜索算法,追踪性垃圾收集):要想了解可达性分析,就必须先知道里面的一个重要的东西:GC Roots。
(1).GC Roots:所谓 "GC Roots"根集合就是一组必须活跃的引用,附上图片一张

在这里插入图片描述

常见的GC Roots有哪些?
1).虚拟机栈中引用的对象
    比如:各个线程被调用的方法中使用到的参数,局部变量等
2).本地方法栈内JNI(通常所说的本地方法)引用的对象
3).方法区中类静态属性引用的对象
  	比如:字符串常量池(String Table)里的引用
4).所有被同步锁synchronized持有的对象
5).java虚拟机内部的引用
	比如:基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointException、OutOfMemoryError),系统类加载器
6).反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调、本地代码缓存等
(2).与引用计数相比,可达性分析的的一些不同:可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数器算法中循环利用的问题,防止内存泄露的发生
(3).基本思路:
1).可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜获被根对象集合所连接的目标对象是否可达
2).使用可达性分析后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
3).如果目标对象没有与任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
4).在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象
(4).实现:在gc被调用之前,会调用对象中的finalize()方法,来对对象进行回收(finalization机制)。由于这是每个对象都拥有的方法,所以将它定义在了Object类中。
finalization机制:
①.基本信息:
                    1).java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
                    2).当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
                    3).finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放的工作,
                    比如关闭文件,套接字和数据库链接等 
②.finalize()方法的一些注意事项:永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括如下三点:

1).在finalize()时可能会导致对象复活
2).finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会
3).一个糟糕的finalize()方法会严重影响GC的性能

③.由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是"非死不可"的,这时候它们暂时处于”缓刑"阶段。 一个无法触及的对象有可能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
1).可触及的:从根节点开始,可以到达这个对象
2).可复活的:对象的所有引用都被释放,但是对象有可能在finalize()方法中复活
3).不可触及的:对象的finalize()方法被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()方法只会被调用一次

以上3种状态中,是由于finalize()方法的存在进行的区分。并且只有在对象不可触及时才可以被回收。

④.可达性分析对垃圾的标记过程:
1).如果对象obj到GC Roots没有引用链,则进行第一次标记
2).进行筛选,判断此对象是否有必要执行finalize()方法   	
      - 如果对象obj没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为"没有必要执行",obj被判定为不可触及的
      - 如果对象obj重写了finalize()方法,且还未执行过,那么obj会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的finalizer线程触发其finalize()方法执行
      - finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。
      - 如果这期间obj在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,obj会被移除"即将回收"集合。
      - 之后,对象会再次出现没有引用存在的的情况。在这个情况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态。
      - 也就是说,一个对象的finalize()方法只会被调用一次
附上在finalize()方法中,对象的复活代码:
package com.cdc.gc.demo;

/**
 * @author cdc
 * @email c925638766@163.com
 * @date 2020/8/4 23:37
 * 测试finalize()方法,测试对象是否可以被复活
 */
public class CanReLiveDemo {
    private static CanReLiveDemo obj;

//    @Override
//    protected void finalize() throws Throwable {
//        super.finalize();
//        System.out.println("调用当前类重写的finalize()方法");
//        obj=this;//待回收的对象在finalize()方法中与引用链重新建立联系
//    }

    public static void main(String[] args) {
        try {
            obj=new CanReLiveDemo();//与堆区的内容建立引用
            obj=null;//断开引用
            System.out.println("第1次 gc");
            System.gc();//显式调用gc()
            Thread.sleep(2000);//由于finalizer线程的优先级低,防止finalize方法没来得及调用
            if (obj==null){
                System.out.println("obj is dead");
            }else {
                System.out.println("obj is alive");
            }
            System.out.println("第2次 gc");
            obj=null;//断开引用
            System.gc();//显式调用gc()
            Thread.sleep(2000);//由于finalizer线程的优先级低,防止finalize方法没来得及调用
            if (obj==null){
                System.out.println("obj is dead");
            }else {
                System.out.println("obj is alive");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

当finalize()方法被注释时,即对象没有重写时,控制台打印的信息:
"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" "-javaagent:F:\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=51714:F:\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;F:\Java自学\jvm\JVMDemo\out\production\JVMDemo;F:\Maven_jars\junit\junit\4.11\junit-4.11.jar;F:\Maven_jars\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar" com.cdc.gc.demo.CanReLiveDemo
第1次 gc
obj is dead
第2次 gc
obj is dead

Process finished with exit code 0

在这里,由于finalize()方法被注释起来了,也就是该对象并没有重写finalize()方法,因此实际上是使用的父类也就是Object类中的的finalize()方法, protected void finalize() throws Throwable { }。在第一次gc时,此时的obj已经与堆中的对象断开了连接,也就是堆中的对象没有与GC Roots搭上线,处于可复活状态,但是在之后调用finalize()方法时发现,并没有重写,所以对象直接变为了不可触及状态,被回收掉了。所以在第一次与第二次gc时,均输出 obj is dead

当finalize()方法没有被注释,即重写了finalize()方法时,控制台打印的信息:
"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" "-javaagent:F:\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=51902:F:\JetBrains\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;F:\Java自学\jvm\JVMDemo\out\production\JVMDemo;F:\Maven_jars\junit\junit\4.11\junit-4.11.jar;F:\Maven_jars\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar" com.cdc.gc.demo.CanReLiveDemo
第1次 gc
调用当前类重写的finalize()方法
obj is alive
第2次 gc
obj is dead

Process finished with exit code 0

在这次的过程中,由于finalize()方法被重写了,在进行第1次gc时,调用finalize()方法,此时通过 obj=this 使得堆中的对象与GC Roots搭上了线,被复活了,因此第1次输出的结果是 obj is alive;但是第2次gc时,由于finalize()方法只能被调用一次,所以此时的对象无法再次复活,输出 obj is dead。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值