JVM - finalize()方法的原理

总结

1.当JVM通过GC Roots可达性分析,判断某对象可以被回收后,会判断是否重写了finalize方法,如果没有,直接回收

2.如果重写了,把该对象放入F-Queue队列,有线程(一个级别很低的daemon线程)专门遍历并执行这些的finalize方法

3.执行finalize()后,等下一次GC时再判断该类是否可被回收,如果是就回收

 

override finalize()函数的风险:

  • 含有override finalize()的对象至少要经历两次GC才能被回收。拉长了对象生命周期,拖慢GC速度,增加了OOM风险
  • finalizer线程运行级别很低,有可能出现finalize速度跟不上对象创建速度,最终可能还是会OOM

 

详细

我们现在来看一下自定义了(override)finalize()的对象(或是某个父类override finalize())是怎样被GC回收的,首先需要注意的是,含有override finalize()的对象A创建要经历以下3个步骤:

  • 创建对象A实例
  • 创建java.lang.ref.Finalizer对象实例F1,F1指向A和一个reference queue
    (引用关系,F1—>A,F1—>ReferenceQueue,ReferenceQueue的作用先卖个关子)
  • 使java.lang.ref.Finalizer的类对象引用F1
    (这样可以保持F1永远不会被回收,除非解除Finalizer的类对象对F1的引用)

经过上述三个步骤,我们建立了这样的一个引用关系:

java.lang.ref.Finalizer–>F1–>A,F1–>ReferenceQueue。GC过程如下所示:

 

有override finalize()对象的minor gc

如上图所示,在发生minor gc时,即便一个对象A不被任何其他对象引用,只要它含有override finalize(),就会最终被java.lang.ref.Finalizer类的一个对象F1引用,等等,如果新生代的对象都含有override finalize(),那岂不是无法GC?没错,这就是finalize()的第一个风险所在,对于刚才说的情况,minor gc会把所有活跃对象以及被java.lang.ref.Finalizer类对象引用的(实际)垃圾对象拷贝到下一个survivor区域,如果拷贝溢出,就将溢出的数据晋升到老年代,极端情况下,老年代的容量会被迅速填满,于是让人头痛的full gc就离我们不远了。

那么含有override finalize()的对象什么时候被GC呢?例如对象A,当第一次minor gc中发现一个对象只被java.lang.ref.Finalizer类对象引用时,GC线程会把指向对象A的Finalizer对象F1塞入F1所引用的ReferenceQueue中,java.lang.ref.Finalizer类对象中包含了一个运行级别很低的daemon线程finalizer来异步地调用这些对象的finalize()方法,调用完之后,java.lang.ref.Finalizer类对象会清除自己对F1的引用。这样GC线程就可以在下一次minor gc时将对象A回收掉。

也就是说一次minor gc中实际至少包含两个操作:

  • 将活跃对象拷贝到survivor区域中
  • 以Finalizer类对象为根,遍历所有Finalizer对象,将只被Finalizer对象引用的对象(对应的Finalizer对象)塞入Finalizer的ReferenceQueue中

可见Finalizer对象的多少也会直接影响minor gc的快慢。

包含有自定义finalizer方法的对象回收过程总结下来,有以下三个风险:

  • 如果随便一个finalize()抛出一个异常,finallize线程会终止,很快地会由于f queue的不断增长导致OOM
  • finalizer线程运行级别很低,有可能出现finalize速度跟不上对象创建速度,最终可能还是会OOM,实际应用中一般会有富裕的CPU时间,所以这种OOM情况可能不太常出现
  • 含有override finalize()的对象至少要经历两次GC才能被回收,严重拖慢GC速度,运气不好的话直接晋升到老年代,可能会造成频繁的full gc,进而影响这个系统的性能和吞吐率。

以上的三点还没有考虑minor gc时为了分辨哪些对象只被java.lang.ref.Finalizer类对象引用的开销,讲完了finalize()原理,我们回头看看最初的那句话:JVM能够保证一个对象在回收以前一定会调用一次它的finalize()方法。

含有override finalize()的对象在会收前必然会进入F QUEUE,但是JVM本身无法保证一个对象什么时候被回收,因为GC的触发条件是需要GC,所以JVM方法不保证finalize()的调用点,如果对象一直不被回收,就一直不调用,而调用了finalize(),也不代表对象就被回收了,只有到了下一次GC时该对象才能真正被回收。另外一个关键点是一次,在调用过一次对象A的finalize()之后,就解除了Finalizer类对象和对象F1之间的引用关系,如果在finalize()中又将对象本身重新赋给另外一个引用(对象拯救),那这个对象在真正被GC前是不会再次调用finalize()的。

总结一下finalize()的两个个问题:

  • 没有析构函数那样明确的语义,调用时间由JVM确定,一个对象的生命周期中只会调用一次
  • 拉长了对象生命周期,拖慢GC速度,增加了OOM风险

回到最初的问题,对于那些需要释放资源的操作,我们应该怎么办?effective java告诉我们,最好的做法是提供close()方法,并且告知上层应用在不需要该对象时一掉要调用这类接口,可以简单的理解这类接口充当了析构函数。当然,在某些特定场景下,finalize()还是非常有用的,例如实现一个native对象的伙伴对象,这种伙伴对象提供一个类似close()接口可能不太方便,或者语义上不够友好,可以在finalize()中去做native对象的析构。不过还是那句话,fianlize()永远不是必须的,千万不要把它当做析构函数,对于一个对性能有相当要求的应用或服务,从一开始就杜绝使用finalize()是最好的选择。

上一篇:谈谈final、finally、finalize的区别


下一篇:学Java入门篇:Java类的基本内容