作者PsMonkey (痞子军团团长)
看板Translate-CS
标题[翻译] 理解 Finalizer
时间Sun Apr 5 14:45:02 2015
原文网址:
https://plumbr.eu/blog/debugging-to-understand-finalizer
译文网址:
http://blog.dontcareabout.us/2015/04/finalizer.html
BBS 版以 markdown 语法撰写
译注:
原文标题是 Debugging to Understand Finalizer
但是那个 debugging 实在有点累赘,所以乾脆略过
不知道有人可以提供更好的翻译 Orz
另外,reference / refer 跟 finalizable / finalize(非专有名词时)
我是保留原文,但不知道用起来对不对,也恳请赐教 Orz
----------------------------------------------------------------------
这篇文章涵盖了一个 Java 的内建功能:**Finalizer**。
这个功能实际上广为人知却也鲜为人知,
取决於你是否仔细看过 `java.lang.Object`。
在 `java.lang.Object` 中有一个叫做 `finalize()` 的 method。
它没有实际的内容,但是它的威能与危险程度
都取决於 JVM 内部如何处置这个 method。
当 JVM 侦测到 class 有 `finalize()` 这个 method,黑魔法就开始了。
所以,我们来弄一个有不同 `finalize()` 的 class,
这样我们就能知道在这种状况下 JVM 会如何处理这个 object。
## 可 finalize 的 class 范例 ##
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() }
);
}
}
}
}
这个程式在一个无穷回圈中建立新的 object。
这些 object 用 `aliveCount` 这个 static 变数
来追踪已经创造出来的 instance 数量。
当建立一个新的 instance 时,`aliveCount` 会增加;
在 GC 之後呼叫 `finalize()`,`aliveCount` 就会减少。
你觉得这段程式会有什麽结果?
如果新建立的 object 没有被 reference,
那应该马上变成可被 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 为样本,
程式在大约 1.2M 个 object 建立之後就炸了
[java.lang.OutOfMemoryError: GC overhead limitt exceeded][1]。
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)
## GC 行为 ##
要搞清楚发生什麽事情,我们必须观察程式在执行期的样子。
所以我们加上 `-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)
从 log 中我们可以看到在几次 minor GC 清除 Eden 之後,
JVM 开始作一堆贵松松 Full GC 去清除 tenured 跟 old 空间。
为什麽会这样?如果 object 没有人 reference,
不是应该所有 instance 老早在 Eden 就死翘翘了吗?
我们的程式出了什麽问题?
原因就在 GC 的所作所为当中,
要了解这一点,我们来作一些小变动:移除 `finalize()`。
现在 JVM 认定这个 class 不需要 finalize 了,行为回归到「正常」。
检视 GC log,我们只看到便宜的 minor GC 一直在运作。

修改後的程式里,Eden(所有 object 的出生地)中的 object
都没人 reference,GC 可以很有效率、一次就把整个 Eden 砍掉重练。
所以 Eden 马上就清空了,无穷回圈也就可以无穷回圈下去。
另一方面,在原本的程式当中就不是这麽回事。
虽然 object 都没有任何 reference,
**JVM 为每一个可被 *finalize* 的 instance 建立各自的 watchdog。
这 watchdog 是 `Finalizer` 的 instance。**
`Finalizer` class 会 reference 这些 instance,
因为有这 reference 链在,所以整团 object 都会存活着。
现在 Eden 已经满了、所有的 object 也还被 reference,
GC 没有选择余地只能把所有东西复制到 Survivor 那里去。
更糟糕的是,如果 Survivor 的剩余空间有限,就会扩散到 Tenured 去。
你可能还记得,GC 在处理 Tenured 是头完全不一样的野兽,
跟清理 Eden 那种「统统丢掉」的方法相比,付出的代价昂贵非常多。
## Finalizer queue ##
只有在 GC 结束之後,JVM 才会知道除了 `Finalizer` 以外
没有其他人 reference 到我们的 instance,
它就会把所有 `Finalizer` 指向的 instance 标记成
「已经准备好可以处理」的状态。
所以 GC 内部会把所有 `Finalizer` object 加进
`java.lang.ref.Finalizer.ReferenceQueue` 这个特别的 queue 中。
只有所有麻烦都搞定,我们的主要 thread 才能进行实际的工作。
现在我们对其中一个 thread 特别有兴趣:`Finalizer` daemon thread。
你可以用 jstack 作 thread dump 来看一下这个 thread:
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` daemon thread 正在执行中。
`Finalizer` 的责任很单纯,
用一个无穷回圈等待新的 instance 出现在
`java.lang.ref.Finalizer.ReferenceQueue` 这个 queue 里头。
当 `Finalizer` thread 侦测到 queue 中有新的 object,
它会取出这个 object、呼叫 `finalize()`、
并移除来自 `Finalizer` 的 reference。
所以在下一次 GC 执行 Finalizer,这个 object 就可以被 GC 掉。
於是,我们有两个无穷回圈在两个不同的 thread 上头运作着。
主 thread 忙着制造新的 object;
这些 object 都有他们自己名为 `Finalizer` 的 watchdog、
都被 GC 加到 `java.lang.ref.Finalizer.ReferenceQueue` 中。
而 `Finalizer` thread 正在处理这个 queue、
把所有 queue 中的 instance 取出来呼叫他们的 `finalize()`。
大多数时候,你可以躲过这件事。
呼叫 `finalize()` 完毕的速度应该要比我们产生新的 instance 的速度快。
所以在很多情况下,`Finalizer` thread 可以赶得上、
并在下一次 GC 倒入更多 `Finalizer` 之前清空 queue。
这件事情显然没有发生在我们的程式中。
为什麽会这样?
`Finalizer` thread 的执行优先等级比主 thread 低。
这代表它能获得的 CPU 时间较少,让它无法赶上 object 建立的步调。
我们的程式码就是这麽回事──
object 建立的速度比 `Finalizer` thread 呼叫各个 `finalize()` 的速度快,
导致所有可用的 heap 都被吃光光。
结果就是看到老朋友 [java.lang.OutOfMemoryError][2]。
如果你还是不相信,拿一个 heap dump 出来看看。
举例来说,当我们的程式启动时加这个参数:
`-XX:+HeapDumpOnOutOfMemoryError`,
会在 Eclipse MAT Dominator Tree 看到这张图:

从这张图可以看到,64M 大的 heap 几乎都被 `Finalizer` 给塞满。
## 结论 ##
可 `finalize` object 的生命周期跟标准行为是完全不同的:
1. JVM 建立 **可 finalize** class 的 instance。
2. JVM 建立 `java.lang.ref.Finalizer` 的 instance,
指向我们新建立的 instance。
3. `java.lang.ref.Finalizer` 会保有刚刚建立的
`java.lang.ref.Finalizer` instance。
这会阻挡下一次 minor GC 收割我们的 object、使它保持在存活的状态。
4. minor GC 无法清空 Eden,并延伸到 Survivor 与/或 Tenured 空间。
5. GC 判定这些 object 有 **finalize** 的资格,
把它们加到 `java.lang.ref.Finalizer.ReferenceQueue` 中。
6. `Finalizer` thread 会处理 queue,
一个接着一个取出 object 然後呼叫它们的 `finalize()`。
7. `finalize()` 被呼叫之後,
`Finalizer` thread 把 `Finalizer` class 中的 reference 移除,
所以下一次 GC 时,object 就有被 GC 掉的资格。
8. `Finalizer` thread 会跟我们的主 thread 竞争,
但是它的优先等级比较低,所以永远赶不上。
9. 程式把所有资源吃乾抹净,炸了 [OutOfMemoryError][2]。
这个故事告诉我们什麽?
下次当你认为 `finalize()` 会比一般的清除、teardown
或是 finally block 还要优先执行时,再想一下吧。
你可能会对你制造出来的乾净程式码感到欣慰,
但是 queue 中不断增加的 **可 finalize** object
会塞爆你的 tenured 与 old generation 空间。
这意味着你需要重新想一想。
[1]:
https://plumbr.eu/outofmemoryerror/gc-overhead-limit-exceeded
[2]:
https://plumbr.eu/outofmemoryerror
--
钱锺书:
说出来的话
http://www.psmonkey.org
比不上不说出来的话
Java 版 cookcomic 版
只影射着说不出来的话
and more......
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 114.43.99.126
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Translate-CS/M.1428216305.A.EBF.html
1F:推 sellgd: 微软译 完成项 05/15 23:06