这篇文章涵盖了Java内置概念之一,称为Finalizer 。 这个概念实际上是众所周知的,也是众所周知的,这取决于您是否有足够的时间来仔细研究一下java.lang.Object类。 就在java.lang.Object本身中,有一个名为finalize()的方法。 该方法的实现是空的,但是基于这种方法的存在,力量和危险都取决于JVM内部行为。
当JVM检测到该类具有finalize()方法时,魔术就开始发生。 因此,让我们继续使用非平凡的finalize()方法创建一个类,这样我们就可以了解JVM在这种情况下处理对象的方式。 为此,让我们开始构建一个示例程序:
终结类的示例
import java.util.concurrent.atomic.AtomicInteger;class Finalizable {static AtomicInteger aliveCount = new AtomicInteger(0);Finalizable() {aliveCount.incrementAndGet();}@Overrideprotected 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 OS X上的JDK 1.7.0_51上,我看到程序因java.lang.OutOfMemoryError而失败:创建约120万个对象后, GC开销限制已超出 :
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 exceededat 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)
从日志中我们可以看到,仅用几个次要GC清理了Eden之后,JVM便转而使用了昂贵得多的Full GC周期来清理使用期限和旧空间。 为什么这样? 由于没有东西引用我们的对象,难道所有实例都不应该在伊甸园中早逝吗? 我们的代码有什么问题?
要理解GC行为的原因,让我们对代码做些微改动,并删除finalize()方法的主体。 现在,JVM检测到不需要终结我们的类,并将行为更改回“正常”状态。 查看GC日志,我们只会看到廉价的次要GC永远运行。
就像在此修改示例中一样,没有任何东西确实指向Eden中的对象(所有对象都在其中诞生),GC可以做非常有效的工作并立即丢弃整个Eden。 因此,我们立即清洗了整个伊甸园,而永无止境的循环将永远持续下去。
另一方面,在我们原始的示例中,情况有所不同。 JVM会为每个Finalized实例创建一个个人看门狗,而不是没有任何引用的对象。 这个看门狗是Finalizer的一个实例 。 而所有这些实例又被Finalizer类引用。 因此,由于有了这个参考链,整个团伙仍然活着。
现在,伊甸园已满,所有对象都已被引用,GC除了将所有内容复制到Survivor空间之外,别无选择。 或更糟糕的是,如果“幸存者”中的自由空间也受到限制,则扩展到“终身制”空间。 您可能还记得,终身制空间中的GC是完全不同的野兽,并且比用于清理伊甸园的“让一切都扔掉”方法昂贵得多。
终结器队列
只有在GC完成之后,JVM才知道除终结器之外,没有任何东西可以引用我们的实例,因此它可以标记所有指向这些实例的终结器以供处理。 因此,GC内部将所有Finalizer对象添加到位于java.lang.ref.Finalizer.ReferenceQueue的特殊队列中。
只有完成所有这些麻烦之后,我们的应用程序线程才能继续进行实际工作。 这些线程之一现在对我们特别有趣- “ Finalizer”守护程序线程。 您可以通过jstack进行线程转储来查看该线程的运行情况:
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: RUNNABLEat 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”守护程序线程正在运行。 “最终确定器”线程是仅具有单一责任的线程。 线程运行一个未终止的循环,该循环被阻塞,等待新实例出现在java.lang.ref.Finalizer.ReferenceQueue队列中。 每当“ Finalizer”线程在队列中检测到新对象时,它将弹出对象,调用finalize()方法并从Finalizer类中删除引用,因此,下次GC运行Finalizer时 ,现在可以将引用的对象设为GCd。
因此,我们现在有两个未终止的循环在两个不同的线程中运行。 我们的主线程正在忙于创建新对象。 这些对象都有自己的个人看门狗,称为终结器 ,它们已由GC添加到java.lang.ref.Finalizer.ReferenceQueue中 。 然后,“ Finalizer ”线程正在处理该队列,从该队列中弹出所有实例,并在这些实例上调用finalize()方法。
在大多数情况下,您会避免这样做。 调用finalize()方法应该比我们实际创建新实例更快。 因此,在许多情况下, “ Finalizer”线程将能够赶上并清空下一个队列,然后再将下一个GC注入更多Finalizer 。 就我们而言,这显然没有发生。
为什么这样? “终结器”线程的运行优先级低于主线程。 这意味着它将减少CPU时间,因此无法跟上正在创建的对象的步伐。 到这里,我们有了对象-创建对象的速度比“ Finalizer”线程能够完成这些对象的finalize()更快,从而消耗了所有可用堆。 结果–我们亲爱的朋友java.lang.OutOfMemoryError的口味不同。
如果您仍然不相信我,请进行堆转储并查看内部。 例如,当使用-XX:+ HeapDumpOnOutOfMemoryError参数启动我们的代码片段时,我在Eclipse MAT Dominator Tree中看到以下图片:
从屏幕快照中可以看到,我的64m堆完全充满了Finalizers 。
结论
综上所述, Finalizable对象的生命周期与标准行为完全不同,即:
- JVM将创建Finalizable对象的实例
- JVM将创建java.lang.ref.Finalizer的实例,指向我们新创建的对象实例。
- java.lang.ref.Finalizer类保留刚刚创建的java.lang.ref.Finalizer实例。 这会阻止下一个较小的GC收集我们的对象并使它们保持活动状态。
- 次要GC无法清洁伊甸园,并扩展到幸存者和/或保有权空间。
- GC检测到这些对象有资格完成,并将这些对象添加到java.lang.ref.Finalizer.ReferenceQueue
- 该队列将由“ Finalizer ”线程处理,一个接一个地弹出对象并调用其finalize()方法。
- 在调用finalize()之后,“ Finalizer ”线程从Finalizer类中删除该引用,因此在下一个GC中可以将对象限定为GCd。
- “ Finalizer ”线程与我们的“ main ”线程竞争,但是由于优先级较低,CPU时间减少,因此永远无法跟上。
- 该程序将耗尽所有可用资源,并抛出OutOfMemoryError 。
故事的道德启示? 下次,当您认为finalize()优于通常的清理,拆除或finally块时,请再考虑一下。 您可能对所生成的干净代码感到满意,但是,不断增长的Finalizable对象队列使您的老一辈人受挫,这可能表明需要重新考虑。
翻译自: https://www.javacodegeeks.com/2014/05/debugging-to-understand-finalizers.html