是否活着

      在堆里存放着的几乎所有的对象实例,垃圾收集器在堆进行回收前,第一件事情就是要确定这些对象之中哪些还"活着",哪些已经"死去"。

引用计数算法

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

      可观来说,引用计数算法虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是,在Java领域至少主流的Java虚拟机里面都没有选用引用计数算法进行内存管理,主要原因是这个看似简单有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
请输入图片描述

Python是如何解决循环引用的?

  • 手动解除:很好理解,就是在合适的实际,解除引用关系
  • 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。

可达性分析算法

      当前主流的商用程序语言的内存管理系统都是通过可达性分析算法来判定对象是否存活。

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

请输入图片描述

GC Roots

      固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池(StringTable)里的引用
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

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

如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入到GC Roots集合中去考虑,才能保证可达性分析的准确性。

对象的finalization机制

      Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。

当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法

      finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。
      永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由如下:

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

生存还是死亡?

      如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收,但事实上,也并非是"非死不可"的,这时候它们暂时处于"缓刑"阶段。一个无法触及的对象有可能在某一个条件下"复活"自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态:

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

      判断一个对象ObjA是否可回收,至少要经历两次标记过程:

  1. 如果对象ObjA到GC Roots没有引用链,则进行第一次标记
  2. 进行筛选,判断对象是否有必要执行finalize()方法:

    • 如果对象ObjA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为"没有必要执行",ObjA被判定为不可触及的
    • 如果对象ObjA重写了finalize()方法,且还未执行过,那么ObjA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行
    • finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记,如果ObjA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,ObjA会被移出"即将回收"集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。

我还能再抢救一下

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("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 InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        SAVE_HOOK = null;
        //对象第一次成功拯救自己
        System.gc();

        //Finalizer线程优先级较低,暂停一下
        Thread.sleep(2000);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("① i am dead!");
        }

        //抢救失败
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(2000);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("② i am dead!");
        }
    }
    /**
     * finalize method executed!
     * i am still alive
     * ② i am dead!
     */
}
Last modification:September 14th, 2020 at 01:43 pm
如果觉得我的文章对你有用,请随意赞赏