通过调试来理解终结器(Finalizers)

查看原文

本文覆盖的是一个java的内建概念,叫做终结器(Finalizers)。这个概念既很好的隐藏了,又广为人知,这取决于你是否费时间来好好的看看java.lang.Object类。在Object中,有一个方法finalize()。该方法的实现是空的,但是jvm内部行为的威力和危险,都通过这样一个方法表现出来。
当jvm检测到一个类有一个finalize()方法,奇迹发生了,那么,就让我们创建一个类并实现finalize()方法,来看看jvm处理这种情况有什么不同。现在,先让我们构建这个例子程序:

Finalizable 类的例子:

import java.util.concurrent.atomic.AtomicInteger;
class Finalizable{
    static AtomicInteger aliveCount = new AtomicInteger(0);
    Finalizable(){
        aliveCount.incrementAndGet();
    }
    @Override
    protected void finalize() throws Throwable{
        Finalizable.aliveCount.decrementAndGet();
    }
    public static void main(String[] args){
        for(int i =0;;i++){
            Finalizable  f = new Finalizable();
            if((i%100_000)==0){
                System.out.format("After creating %d objects, %d are still alive. %n",new Object[]{i,Finalizable.aliveCount.get()});
            }
        }
    }
}

这个例子在一个无法停止的循环中不停的创建新对象。这些对象使用静态变量aliveCount来跟踪当前已经创建的对象数目。当一个新的对象被创建,计数器加一,而当GC后,finalize()方法被调用后,计数器减一。
那么,你认为这样的一段简单代码结果是怎样呢?因为我们创建的对象不会在别的地方被引用,它们应该立即可以被GC回收的。因此,你可能认为代码会不停的执行下去,并有类似如下的输出:

After creating 345,000,000 objects, 0 are still alive.
After creating 345,100,000 objects, 0 are still alive.
After creating 345,200,000 objects, 0 are still alive.
After creating 345,300,000 objects, 0 are still alive.

显然不是这样的,真相完全不同,例如在作者的Mac机器上(JDK1.7.0_51),会有java.lang.OutOfMemoryError:GC overhead limit exceeded,而这时已经创建了1.2M个对象了:

After creating 900,000 objects, 791,361 are still alive.
After creating 1,000,000 objects, 875,624 are still alive.
After creating 1,100,000 objects, 959,024 are still alive.
After creating 1,200,000 objects, 1,040,909 are still alive.
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.ref.Finalizer.register(Finalizer.java:90)
    at java.lang.Object.(Object.java:37)
    at eu.plumbr.demo.Finalizable.(Finalizable.java:8)
    at eu.plumbr.demo.Finalizable.main(Finalizable.java:19)

垃圾收集行为

要理解发生了什么,我们要看看我们的代码在运行时的样子,我们可以通过开启-XX:+PringGCDetial标志来运行例子代码:

[GC [PSYoungGen: 16896K->2544K(19456K)] 16896K->16832K(62976K), 0.0857640 secs] [Times: user=0.22 sys=0.02, real=0.09 secs] 
[GC [PSYoungGen: 19440K->2560K(19456K)] 33728K->31392K(62976K), 0.0489700 secs] [Times: user=0.14 sys=0.01, real=0.05 secs] 
[GC-- [PSYoungGen: 19456K->19456K(19456K)] 48288K->62976K(62976K), 0.0601190 secs] [Times: user=0.16 sys=0.01, real=0.06 secs] 
[Full GC [PSYoungGen: 16896K->14845K(19456K)] [ParOldGen: 43182K->43363K(43520K)] 60078K->58209K(62976K) [PSPermGen: 2567K->2567K(21504K)], 0.4954480 secs] [Times: user=1.76 sys=0.01, real=0.50 secs] 
[Full GC [PSYoungGen: 16896K->16820K(19456K)] [ParOldGen: 43361K->43361K(43520K)] 60257K->60181K(62976K) [PSPermGen: 2567K->2567K(21504K)], 0.1379550 secs] [Times: user=0.47 sys=0.01, real=0.14 secs] 
--- cut for brevity---
[Full GC [PSYoungGen: 16896K->16893K(19456K)] [ParOldGen: 43351K->43351K(43520K)] 60247K->60244K(62976K) [PSPermGen: 2567K->2567K(21504K)], 0.1231240 secs] [Times: user=0.45 sys=0.00, real=0.13 secs] 
[Full GCException in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
 [PSYoungGen: 16896K->16866K(19456K)] [ParOldGen: 43351K->43351K(43520K)] 60247K->60218K(62976K) [PSPermGen: 2591K->2591K(21504K)], 0.1301790 secs] [Times: user=0.44 sys=0.00, real=0.13 secs] 
    at eu.plumbr.demo.Finalizable.main(Finalizable.java:19)

从日志中我们看到,在几次minor GC清理eden区,JVM转而做了多次更加昂贵的Full GC来清理持久区。为什么会这样,不是应该所有的实例都在eden区死掉吗?因为我们的对象没有被引用啊?我们代码哪里错了?
为了理解这个,GC发生这种行为的原因,现在让我们移除代码中finalize()方法。现在JVM检测到我们的类不需要终结(没有自定义终结逻辑),从而将行为转为正常行为(默认的终结逻辑)。再看下GC的日志就看到只有便宜的minor GC,而且一直运行下去。
因为在这个修改后的例子中,没有引用eden区的对象(所有对象的出生区),GC可以很高效的清理并且可以一次清掉整个eden区。因此会立即清理掉整个eden区,从而使代码无限执行下去。
而在我们原先的例子中,情况却不同,JVM会为每一个Finalizable实例创建一个看门狗,这个看门狗就是一个Finalizer的实例。这些实例都被Finalizer类所引用,因此,由于这里的引用关系,所有的对象都会存活。
当eden区满了,而所有对象都被引用着,GC只能将它们复制到Survivor区,而更糟糕的是:如果Survivor区空间有限,这又会扩展到Tenured(老年区)。你可能还记得,GC在老年区是一个完全不同的野兽,它会执行比清理eden区代价更高的操作。

终结器队列

只有在GC结束,JVM才能知道除了Finalizer,那些对象没有其它的引用,因此,它可以标记所有指向这些实例的Finalizer准备来处理。因此,GC内部会将所有的Finalizer对象加入到一个特殊的队列:
java.lang.ref.Finalizer.ReferenceQueue.
只有当所有的麻烦都处理掉,我们的程序线程才能继续处理实际的工作。这其中有一个线程我们比较感兴趣——Finalizer后台线程。你可以通过jstack来dump该线程,来看看这个线程的动作:

My Precious:~ demo$ jps
1703 Jps
1702 Finalizable
My Precious:~ demo$ jstack 1702

--- cut for brevity ---
"Finalizer" daemon prio=5 tid=0x00007fe33b029000 nid=0x3103 runnable [0x0000000111fd4000]
   java.lang.Thread.State: RUNNABLE
    at java.lang.ref.Finalizer.invokeFinalizeMethod(Native Method)
    at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:101)
    at java.lang.ref.Finalizer.access$100(Finalizer.java:32)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:190)
--- cut for brevity ---

从上面我们看到Finalizer后台线程正在运行,Finalizer线程只有一个职责,不断的循环等待java.lang.ref.Finalizer.ReferenceQueue队列里有新的实例出现。当Finalizer线程检测到队列中有新对象,它会弹出该对象,调用该对象的finalize()方法,然后从Finalizer类中移除该引用,那么下一轮GC运行Finalizer,并且该引用对象就可以被回收了。
因此,现在我们有两个不同的线程都在无限循环着。我们的主线程不停的创建新对象。这些对象都有它自己的看门狗对象,叫做Finalizer,该对象然后被GC添加到java.lang.ref.Finalizer.ReferenceQueue队列。然后Finalizer线程负责处理这个队列,从队列中弹出实例并调用其finalize()方法。
大多数时间你可能会认为:调用finalize()方法应该比我们实际创建一个新对象更快,因此,很多情况下,Finalizer线程能赶上在下次GC带来更多Finalizer对象前,处理掉当前队列中的实例。但是在我们的例子中,这显然没有发生。
为什么会这样呢?Finalizer线程的执行优先级比主线程低。这意味着它拥有更少的CPU时间来处理,因此,它可能跟不上对象的创建速度。因此我们也就有了对象的创建速度比Finalizer线程终结它们快的结论,而这会因此所有可用堆内存被耗尽。结果就是不同口味的java.lang.OutOfMemoryError异常。

结论

所以回顾一下,Finalizable对象与标准行为有完全不同的生命周期:
- JVM创建Finalizable对象
- JVM会创建一个java.lang.ref.Finalizer对象实例,指向我们新创建的对象
- java.lang.ref.Finalizer类持有刚创建的java.lang.ref.Finalizer实例,这阻止了下一轮GC回收掉我们的对象,使得它们存活。
- minor GC不能清理到eden区,并扩展到存活区(survivor)和持久区(tenured)
- GC检测到该对象可以终结,并将它们添加到java.lang.ref.Finalizer.ReferenceQueue
- 该队列由Finalizer线程处理,一个一个的弹出对象并调用其finalize()方法
- finalize()方法被调用后,Finalizer线程从Finalizer类中移除该引用,因此在下一轮GC时,该对象会被回收
- Finalier 线程和主线程竞争CPU资源,但由于优先级低,所以处理速度跟不上主线程创建对象的速度。
- 程序消耗掉所有的可用资源并抛出OutOfMemoryError。

这个故事的寓意?下一次,当你考虑使用finalize()方法来做超出一般的清理,卸载或最后的操作,仔细考虑。你可能会乐于看到你创建的clean code,但是不断增加的Finalizable 对象的队列会耗尽你的持久区和老年代,而这要认真考虑。

posted @ 2017-03-14 13:15  JintaoXIAO  阅读(2244)  评论(0编辑  收藏  举报