本文覆蓋的是一個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 對象的隊列會耗盡你的持久區和老年代,而這要認真考慮。