【Finalizer】Java的Finalizer引起的內存溢出

Java的Finalizer引起的內存溢出

本文介紹的是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則徹底不一樣,它採用的是一種開銷更大的方式。

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隊列也許會撐爆你的年老代,你須要從新再考慮一下你的方案。

原創文章轉載請註明出處:Java的Finalizer引起的內存溢出 英文原文連接

相關文章
相關標籤/搜索