垃圾收集算法-如何判定对象死亡

在堆中存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先要区分出那些对象存活,哪些对象死亡,只有被标记为死亡的对象,GC才会在垃圾回收时释放其所占用的内存空间,这个过程被称为垃圾标记阶段

在jvm中,当一个对象已经不再被任何存活的对象继续引用时,就可以被宣判死亡,判断对象存活一般有两种方式:

  • 引用计数法
  • 可达性算法

1.不被java采用的引用计数法

引用计数法(Reference Counting),其实现过程相对简单:对每个对象保存一个整形引用计算器属性,用于记录对象被引用的情况,举个例子:对一个对象A,只要有一个对象引用了A,则A的引用计数器加1,当引用失效,引用计数器减1,当A计数器为0,表示对象A不能再被引用,可进行回收
其优点就是实现简单,垃圾对象易于辨识,判定效率高,回收没有延迟,虽然它有这么多优点,但为什么没有被Java采用?,是因为它有许多缺点:

  • 需要单独的字段存储计数器,增加了存储的空间开销
  • 伴随着加法减法操作,带来了时间开销
  • 无法处理循环引用的问题,这是它的一个致命缺陷,这样可能导致内存泄漏

那么什么是循环引用呢?看下面的图:
**加粗样式**
简单来说:对象p指向了一个链表对象,而链表对象内部的计数器rc被循环引用,当p指向null时,由于循环引用导致rc不能清零导致无法被回收,进而导致内存泄漏,虽然java不采用这种方式,但某些其他语言采用了,比如Python,Python提供了解决循环引用导致内存泄漏的解决方案:

  • 手动解除
  • 使用弱引用

2.可达性算法

对于这种算法,有许多不同的称呼,比如根搜索算法追踪式垃圾收集(Tracing Garbage Collection),是java,c#等语言采用的判定对象死亡的垃圾收集算法,该算法可以有效解决循环引用问题,防止内存泄漏,其基本思路是:

  • 以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所链接的对象是否可达
  • 使用可达性算法后,内存中的存活对象都会被根对象直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
  • 在可达性分析算法中,只有能被根对象集合直接或间接链接的对象才是存活对象

在这里插入图片描述
在java中,GC Roots包括以下几类元素:

  • 虚拟机栈中引用的对象(局部变量表中的引用指向堆中的对象),比如各个线程被调用的方法使用到的参数,局部变量等,这是最为常见的一种
  • 方法区中类静态属性引用的对象,比如java类的引用类型静态变量
  • 方法区中常量引用的对象,比如字符串常量池里的引用
  • 本地方法栈引用的对象
  • 所有被同步锁synchronized持有的对象
  • jvm虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的异常,系统类加载器
除了这些固定的GC Roots集合以外,根据用户所选的垃圾收集器以及当前回收的内存区域不同,某些对象可临时性的加入GC Roots集合,比如:分代收集和局部回收

3.对象的finalization机制

Java语言提供了对象终止(finalization)机制来允许开发人员提供对象销毁前自定义处理逻辑,当垃圾回收器发现没有一个引用指向对象,在回收这个对象之前先会调用这个对象的finalize()方法,这个方法是Object类中的方法:

  • protected void finalize() throws Throwable { }

该方法允许子类重写,用于在对象被回收时进行资源释放,比如关闭数据库链接等,该方法在GC进行垃圾回收时由gc线程调用
注意:永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,因为在执行这个方法的时候可能导致对象复活,并且该方法执行的时间时没有保障的,它完全由GC线程决定,极端情况下若不发生GC,则该方法永远不会被调用,一个糟糕的finalize()会严重影响GC性能

正是由于finalize()的存在,虚拟机中的对象一般处于三种状态,可触及的,可复活的,不可触及的

  • 可触及的,从根节点开始可以到达这个对象
  • 不可触及的,对象的finalize()被调用,并且没有复活,则进入不可触及状态,在这种状态下对象不可能复活,因为finalize()只能被调用一次
  • 可复活的,对象所有引用都被释放,但对象在finalize()中可能复活

这三种状态是由于finalize()的存在而进行的区分,只有对象在不可触及状态下才能被回收,通过GC垃圾回收器提供的Finalizer线程来处理,该线程优先级比较低

如果从所有根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说此对象需要被回收,但事实上,对象也并非必须立刻进行回收,一个无法触及的对象可能在某一条件下‘复活’自己,在这种情况下再进行回收就显得不合理了

我们通过一个例子来说明这个方法:

public class Alive {

    static Alive alive;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用重写的finalize方法");
        alive = this;
    }

    public static void main(String[] args) {
        try {
            alive = new Alive();
            alive = null;//使对象没有引用
            System.gc();//触发回收
            System.out.println("第一次gc");
            Thread.sleep(2000);//等待Finalizer线程执行
            if(alive == null){
                System.out.println("对象已死");
            }else{
                System.out.println("对象存活");
            }
            System.out.println("第二次gc");
            alive = null;
            System.gc();//触发回收
            Thread.sleep(2000);//等待Finalizer线程执行
            if(alive == null){
                System.out.println("对象已死");
            }else{
                System.out.println("对象存活");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

finalize()注释前后分别进行测试:
注释前:

第一次gc
对象已死
第二次gc
对象已死

注释后:

调用重写的finalize方法
第一次gc
对象存活
第二次gc
对象已死

对于以上结果我们可以做出如下分析:

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章