本文介紹的是Java裏一個內建的概念,Finalizer。你可能對它對數家珍,但也可能從未聽聞過,這得看你有沒有花時間完整地看過一遍java.lang.Object類了。在java.lang.Object裏面就有一個finalize()的方法。這個方法的實現是空的,不過一旦實現了這個方法,就會觸發JVM的內部行爲,威力和危險並存。html
若是JVM發現某個類實現了finalize()方法的話,那麼見證奇蹟的時刻到了。咱們先來建立一個實現了這個非凡的finalize()方法的類,而後看下這種狀況下JVM的處理會有什麼不一樣。咱們先從一個簡單的示例程序開始:java
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。小程序
你以爲這小段代碼的輸出結果會是怎樣的呢?因爲新建立的對象很快就沒人引用了,它們立刻就能夠被GC回收掉。所以你可能會認爲這段程序能夠不停的運行下去,:ide
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異常退出了。atom
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選項再運行一次看看:spa
[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在不停的運行。debug
由於修改後的這段程序中,的確沒有人引用到了新生代的這些剛建立的對象。所以Eden區很快就被清空掉了,整個程序能夠一直的執行下去。日誌
另外一方面,在早先的那個例子中狀況則有些不一樣。這些對象並不是沒人引用 ,JVM會爲每個Finalizable對象建立一個看門狗(watchdog)。這是Finalizer類的一個實例。而全部的這些看門狗又會爲Finalizer類所引用。因爲存在這麼一個引用鏈,所以整個的這些對象都是存活的。code
那如今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 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,而這個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隊列也許會撐爆你的年老代,你須要從新再考慮一下你的方案。
原創文章轉載請註明出處:Java的Finalizer引起的內存溢出 英文原文連接