本文介绍的是Java里一个内建的概念,Finalizer。你可能对它对数家珍,但也可能从未听闻过,这得看你有没有花时间完整地看过一遍java.lang.Object类了。在java.lang.Object里面就有一个finalize()的方法。这个方法的实现是空的,不过一旦实现了这个方法,就会触发JVM的内部行为,威力和危险并存。java
若是JVM发现某个类实现了finalize()方法的话,那么见证奇迹的时刻到了。咱们先来建立一个实现了这个非凡的finalize()方法的类,而后看下这种状况下JVM的处理会有什么不一样。咱们先从一个简单的示例程序开始:小程序
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来跟踪一共建立了多少个实例。每建立了一个新对象,计数器会加1,一旦GC完成后调用了finalize()方法,计数器会跟着减1。ide
你以为这小段代码的输出结果会是怎样的呢?因为新建立的对象很快就没人引用了,它们立刻就能够被GC回收掉。所以你可能会认为这段程序能够不停的运行下去,:atom
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 OS X的JDK 1.7.0_51上,程序大概在建立了120万个对象后就抛出java.lang.OutOfMemoryError: GC overhead limitt exceeded异常退出了。spa
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:+PrintGCDetails选项再运行一次看看:线程
[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)
从日志中能够看到,少数几回的Eden区的新生代GC事后,JVM开始采用更昂贵的Full GC来清理老生代和持久代的空间。为何会这样?既然已经没有人引用这些对象了,为何它们没有在新生代中被回收掉?代码这么写有什么问题吗?日志
要弄清楚GC这个行为的缘由,咱们先来对代码作一个小的改动,将finalize()方法的实现先去掉。如今JVM发现这个类没有实现finalize()方法了,因而它切换回了”正常”的模式。再看一眼GC的日志,你只能看到一些廉价的新生代GC在不停的运行。code
由于修改后的这段程序中,的确没有人引用到了新生代的这些刚建立的对象。所以Eden区很快就被清空掉了,整个程序能够一直的执行下去。orm
另外一方面,在早先的那个例子中状况则有些不一样。这些对象并不是没人引用 ,JVM会为每个Finalizable对象建立一个看门狗(watchdog)。这是Finalizer类的一个实例。而全部的这些看门狗又会为Finalizer类所引用。因为存在这么一个引用链,所以整个的这些对象都是存活的。对象
那如今Eden区已经满了,而全部对象又都存在引用,GC没辙了只能把它们全拷贝到Suvivor区。更糟糕的是,一旦连Survivor区也满了,只能存到老生代里面了。你应该还记得,Eden区使用的是一种”抛弃一切”的清理策略,而老生代的GC则彻底不一样,它采用的是一种开销更大的方式。
只有在GC完成后,JVM才会意识到除了Finalizer对象已经没有人引用到咱们建立的这些实例了,所以它才会把指向这些对象的Finalizer对象标记成可处理的。GC内部会把这些Finalizer对象放到java.lang.ref.Finalizer.ReferenceQueue这个特殊的队列里面。
完成了这些麻烦事以后,咱们的应用程序才能继续往下走。这里有个线程你必定会很感兴趣——Finalizer守护线程。经过使用jstack进行thread dump能够看到这个线程的信息。
My Precious:~ demo$ jps Jps 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,而这个Finalizer对象会被添加到一个java.lang.ref.Finalizer.ReferenceQueue中。Finalizer线程会负责处理这个队列,它将全部的对象弹出,而后调用它们的finalize()方法。
不少时候你可能磁不到内存溢出这种状况。finalize()方法的调用会比你建立新对象要早得多。所以大多数时候,Finalizer线程可以赶在下次GC带来更多的Finalizer对象前清空这个队列。但咱们这个例子当中,显然不是这样。
为何会出现溢出?由于Finalizer线程和主线程相比它的优先级要低。这意味着分配给它的CPU时间更少,所以它的处理速度无法遇上新对象建立的速度。这就是问题的根源——对象建立的速度要比Finalizer线程调用finalize()结束它们的速度要快,这致使最后堆中全部可用的空间都被耗尽了。结果就是——咱们亲爱的小伙伴java.lang.OutOfMemoryError会以不一样的身份出如今你面前。
若是你仍然不相信个人话,dump一下堆内存,看下它里面有什么。好比说,你可使用-XX:+HeapDumpOnOutOfMemoryError参数启动咱们这个小程序,在个人Eclipse中的MAT Dominator Tree中我看到的是下面这张图:
看到了吧,我这个64M的堆全给Finalizer对象给占满了。
回顾一下,Finalizable对象的生命周期和普通对象的行为是彻底不一样的,列举以下:
这篇文章想告诉咱们什么?下回若是你考虑使用finalize()方法,而不是使用常规的方式来清理对象的话,最好多想一下。你可能会为使用了finalize()方法写出的整洁的代码而沾沾自喜,可是不停增加的Finalizer队列也许会撑爆你的年老代,你须要从新再考虑一下你的方案。