美文网首页jvmjavaAndroid知识
深入分析Object.finalize方法的实现原理

深入分析Object.finalize方法的实现原理

作者: 美团Java | 来源:发表于2017-02-18 17:58 被阅读4622次

    简书 占小狼
    转载请注明原创出处,谢谢!

    “物有本末,事有始终。知其先后,则近道矣”

    finalize

    如果类中重写了finalize方法,当该类对象被回收时,finalize方法有可能会被触发,下面通过一个例子说明finalize方法对垃圾回收有什么影响。

    public class FinalizeCase {
    
        private static Block holder = null;
    
        public static void main(String[] args) throws Exception {
            holder = new Block();
            holder = null;
            System.gc();
            //System.in.read();
        }
    
        static class Block {
            byte[] _200M = new byte[200*1024*1024];
        }
    }
    

    Block类中声明一个占用内存200M的数组,是为了方便看出来gc之后是否回收了Block对象,执行完的gc日志如下:

    从gc日志中可以看出来,执行完System.gc()之后,Block对象被如期的回收了,如果在Block类中重写了finalize方法,会是一样的结果么?

    static class Block {
        byte[] _200M = new byte[200*1024*1024];
        @Override
        protected void finalize() throws Throwable {
            System.out.println("invoke finalize");
        }
    }
    

    执行完成gc日志如下:

    和之前的gc日志进行比较,发现finalize方法确实被触发了,但是Block对象还在内存中,并没有被回收,这是为什么?

    下面对finalize方法的实现原理进行分析。

    finalize实现原理

    JVM源码分析之Java对象的创建过程》一文中分析了Java对象创建的整个过程,代码实现如下:

    对象的初始化过程会对has_finalizer_flagRegisterFinalizersAtInit进行判断,如果类重写了finalize方法,且方法体不为空,则调用register_finalizer函数,继续看register_finalizer函数的实现:

    其中Universe::finalizer_register_method()缓存的是jdkjava.lang.ref.Finalizer类的register方法,实现如下:

    在jvm中通过JavaCalls::call触发register方法,将新建的对象O封装成一个Finalizer对象,并通过add方法添加到Finalizer链表头。

    对象OFinalizer类的静态变量unfinalized有联系,在发生GC时,会被判定为活跃对象,因此不会被回收

    FinalizerThread线程

    Finalizer类的静态代码块中会创建一个FinalizerThread类型的守护线程,但是这个线程的优先级比较低,意味着在cpu吃紧的时候可能会抢占不到资源执行。

    FinalizerThread线程负责从ReferenceQueue队列中获取Finalizer对象,如果队列中没有元素,则通过wait方法将该线程挂起,等待被唤醒

    如果返回了Finalizer对象,执行对象的runFinalizer()方法,其实可以发现:在runFinalizer()方法中主动捕获了异常,即使在执行finalize方法抛出异常时,也没有关系。

    通过hasBeenFinalized方法判断该对象是否还在链表中,并将该Finalizer对象从链表中删除,这样下次gc时就可以把原对象给回收掉了,最后调用了native方法invokeFinalizeMethod,其中invokeFinalizeMethod方法最终会找到并执行对象的finalize方法。

    ReferenceHandler线程

    有个疑问:既然FinalizerThread线程是从ReferenceQueue队列中获取Finalizer对象,那么Finalizer对象是在什么情况下才会被插入到ReferenceQueue队列中?

    Finalizer的祖父类Reference中定义了ReferenceHandler线程,实现如下:

    pending被设置时,会调用ReferenceQueueenqueue方法把Finalizer对象插入到ReferenceQueue队列中,接着通过notifyAll方法唤醒FinalizerThread线程执行后续逻辑,实现如下:

    pending字段什么时候会被设置?

    在GC过程的引用处理阶段,通过oopDesc::atomic_exchange_oop方法把发现的引用列表设置在pending字段所在的地址

    Finalizer导致的内存泄漏

    平常使用的Socket通信,SocksSocketImpl的父类重写了finalize方法

    这么做主要是为了确保在用户忘记手动关闭socket连接的情况下,在该对象被回收时能够自动关闭socket来释放一些资源,但是在开发过程中,真的忘记手动调用了close方法,那么这些socket对象可能会因为FinalizeThread线程迟迟没有执行到这些对象的finalize方法,而导致一直占用某些资源,造成内存泄露。

    个人公众号


    相关文章

      网友评论

      • bcf7c0301e8f:应该是有些问题,博主愿意继续一起探讨下吗?
        有几个问题:
        1.finalize覆写问题,super.finalize
        2.根据gc日志判断不够准确,根据jvm机制,垃圾回收存在不确定性,即便是执行了system.gc
        3.根据书上所讲,在对象被回收并且在finalize中引用自身时,会逃过一次回收,但是不会有第二次,和你的描述有异议
      • yuanfy:个人感觉文章最后应该来个总结
      • 莫那一鲁道:小狼,这些cpp代码在jdk中吗?
        美团Java:@stateIs0 是hotspot的源码
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/5zy1nu 欢迎点赞支持!
        欢迎订阅《java进阶之路》https://toutiao.io/subjects/85249

      本文标题:深入分析Object.finalize方法的实现原理

      本文链接:https://www.haomeiwen.com/subject/mbqhwttx.html