日暮途远

日暮途远,涸辙难行;东隅已逝,桑榆非晚

Java-Finalizer的那些犄角旮旯

JDK里面的存在一个非常有争议类Finalizer,该类结合Object对象的里面的finalize方法给我们制造了不少的麻烦,下面就来解析下这个troubler的原理。

1. object的finalize方法

jdk中的object对象中存在一个protected方法finalize,该类的功能类似于c++中的析构函数,如果我们new的对象在子类中override该方法的话,那在对象被使用完成之后操作系统进行gc的时候会调用该finalize方法,实现对象生命周期结束前的最后一步。

2. finalize执行(Finalizer宿主机制)

在实际运行环境中,并不是所有的对象都会去执行finalize方法,jdk会记录override过该方法的类并将这些类的实例放入到Finalizer宿主的一个队列中,该队列会有专门的线程解析这些队列中的对象,能够及时的把对象做到回收,下面从代码角度具体分析这些步骤。

Finalizer初始化

从Finalizer类可以看出,该类已经是final所以不可能对其进行实现,并且Finalizer的constructor也为private的,无法通过new的方式来创建对象。

但是该类中包含了一个静态static的register方法,该方法不断地把把新创建的对象添加到对象链中,新的对象方法Finalizer中的next中,旧的则为prev,典型的c++中指针的思想。

但何时才会调用该方法把对象放入到这个对象链中呢?一般jvm在创建对象时有两个步骤,一是分配内存空间,一个是调用构造函数。jvm可以使用者两个步骤中的任意一个来将对象传给Finalizer,为此jvm还专门配置了一个参数-XX:-RegisterFinalizersAtInit来决定是在哪个步骤调用,默认为true,true即在调用构造函数时调用register,否则就在分配内存空间时调用register。

Finalizer GC回收

在各种实现了finalize方法的对象被放入到Finalizer的对象链之后,对象在被使用完成之后相应的内存要被回收,而这时实现了finalize的对象却被引用在了对象链中没有被释放,那这个对象链的释放原理是怎样子的呢?
为了解决回收问题,Finalizer专门提供了一个用来专门回收的线程:FinalizerThread,我们一般称该线程为watchdog(或守护线程),watchdog在初始化时直接被创建:

从代码中看出该线程运行的priority是被降低过的,这个直接导致了很多问题,后续再解释。

而从该线程执行来看,只有在vm启动完成之后该线程才会开始执行回收的任务,回收的任务是一个永不停止的for循环(即使产生异常),执行一次需要先把对象从queue中移除,移除之后执行finalize的方法完成该对象的最后一步。
先从queue中移除然后执行finalize方法也避免了重复执行的可能性

核心部分是在jla.invokeFinalize,查看源码只提供了接口,且是 static native ,是从签名可以看出是调用了JNI来完成的:

在运行完成之后这些对象就静等被回收了。

在实际运行过程中,queue队列中的对象时如何被插入进去的呢?一般情况下jvm发起GC的时候都会去扫描这些这些实现了finalize方法的对象,如果发现该对象只被Finalizer对象引用就表示该对象离被回收不远了,jvm会把该对象加入到ReferenceQueue队列中,FinalizerThread不停的在做循环检查ReferenceQueue中是否有数值,一旦有数值被读入,thread立马执行finalize方法完成该对象的最后一步,下一次执行GC时该对象即被回收。

3. Finalizer-GC产生的问题

一般情况下jvm中对象的产生速度低于回收的速度。但是在部分特殊情况下,会产生灾难性后果。
如:有一个对象A实现了finalize接口,而有一个请求中正好进行了new A的操作。某一天搞活动,大量的请求不断地蜂拥而至,请求的次数随时间推移也逐渐变多,直接导致:

  • 执行finalize方法的线程FinalizerThread优先级比较低,竞争不过主线程,所以导致ReferenceQueue中大量的对象被引用得不到释放,长时间无法被回收,被jvm从年轻代转移到老年代,最终触发Full GC.
  • 大量的实际无用的对象被放在内存中最终导致内存吃紧产生oom。

解决方案:

  • 避免使用finalize;
  • 让finalize的操作变得越少越好,处理时间越少越好,更不推荐finalize之间有线程锁(不推荐);
  • new出来的对象不要每次都抛弃掉,后续重复使用,尽量减少调用finalize的次数;
  • 调整FinalizerThread的优先级,不知是否会产生新的问题。

 

点赞
  1. 匿名说道:

    new出来的对象不要每次都抛弃掉

    ,这个确实要注意,能用对象池实现的方式最好啦 :evil:

发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">