java問題之1:Java的Finalizer引發的內存溢出


本文介紹的是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(); } @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回收掉。因此你可能會認為這段程序可以不停的運行下去,:

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異常退出了。

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在不停的運行。

Java堆分區

因為修改后的這段程序中,的確沒有人引用到了新生代的這些剛創建的對象。因此Eden區很快就被清空掉了,整個程序可以一直的執行下去。

另一方面,在早先的那個例子中情況則有些不同。這些對象並非沒人引用 ,JVM會為每一個Finalizable對象創建一個看門狗(watchdog)。這是Finalizer類的一個實例。而所有的這些看門狗又會為Finalizer類所引用。由於存在這么一個引用鏈,因此整個的這些對象都是存活的。

那現在Eden區已經滿了,而所有對象又都存在引用,GC沒轍了只能把它們全拷貝到Suvivor區。更糟糕的是,一旦連Survivor區也滿了,只能存到老生代里面了。你應該還記得,Eden區使用的是一種”拋棄一切”的清理策略,而老生代的GC則完全不同,它采用的是一種開銷更大的方式。

Finalizer隊列

只有在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對象的生命周期和普通對象的行為是完全不同的,列舉如下:

  • JVM創建Finalizable對象
  • JVM創建 java.lang.ref.Finalizer實例,指向剛創建的對象。
  • java.lang.ref.Finalizer類持有新創建的java.lang.ref.Finalizer的實例。這使得下一次新生代GC無法回收這些對象。
  • 新生代GC無法清空Eden區,因此會將這些對象移到Survivor區或者老生代。
  • 垃圾回收器發現這些對象實現了finalize()方法。因為會把它們添加到java.lang.ref.Finalizer.ReferenceQueue隊列中。
  • Finalizer線程會處理這個隊列,將里面的對象逐個彈出,並調用它們的finalize()方法。
  • finalize()方法調用完后,Finalizer線程會將引用從Finalizer類中去掉,因此在下一輪GC中,這些對象就可以被回收了。
  • Finalizer線程會和我們的主線程進行競爭,不過由於它的優先級較低,獲取到的CPU時間較少,因此它永遠也趕不上主線程的步伐。
  • 程序消耗了所有的可用資源,最后拋出OutOfMemoryError異常。

這篇文章想告訴我們什么?下回如果你考慮使用finalize()方法,而不是使用常規的方式來清理對象的話,最好多想一下。你可能會為使用了finalize()方法寫出的整潔的代碼而沾沾自喜,但是不停增長的Finalizer隊列也許會撐爆你的年老代,你需要重新再考慮一下你的方案。

轉載:https://www.cnblogs.com/benwu/articles/5812903.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM