欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java的Finalizer引發(fā)的內(nèi)存溢出問題及解決

 更新時間:2025年03月07日 09:27:28   作者:冰花ぃ雪魄  
本文介紹了Java中的Finalizer機制,解釋了當(dāng)類實現(xiàn)finalize()方法時,JVM的行為和潛在的風(fēng)險,通過一個示例程序,展示了實現(xiàn)finalize()方法會導(dǎo)致大量對象存活,最終引發(fā)OutOfMemoryError,文章分析了GC日志,解釋了Finalizer線程和主線程之間的競爭

Java的Finalizer引發(fā)的內(nèi)存溢出

本文介紹的是Java里一個內(nèi)建的概念,F(xiàn)inalizer。你可能對它對數(shù)家珍,但也可能從未聽聞過,這得看你有沒有花時間完整地看過一遍java.lang.Object類了。在java.lang.Object里面就有一個finalize()的方法。這個方法的實現(xiàn)是空的,不過一旦實現(xiàn)了這個方法,就會觸發(fā)JVM的內(nèi)部行為,威力和危險并存。

如果JVM發(fā)現(xiàn)某個類實現(xiàn)了finalize()方法的話,那么見證奇跡的時刻到了。我們先來創(chuàng)建一個實現(xiàn)了這個非凡的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() });
                    }
          }
     }
}

這個程序使用了一個無限循環(huán)來創(chuàng)建對象。它同時還用了一個靜態(tài)變量aliveCount來跟蹤一共創(chuàng)建了多少個實例。每創(chuàng)建了一個新對象,計數(shù)器會加1,一旦GC完成后調(diào)用了finalize()方法,計數(shù)器會跟著減1。

你覺得這小段代碼的輸出結(jié)果會是怎樣的呢?由于新創(chuàng)建的對象很快就沒人引用了,它們馬上就可以被GC回收掉。因此你可能會認(rèn)為這段程序可以不停的運行下去:

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.

顯然結(jié)果并非如此。現(xiàn)實的結(jié)果完全不同,在我的Mac OS X的JDK 1.7.0_51上,程序大概在創(chuàng)建了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)

垃圾回收的行為

想弄清楚到底發(fā)生了什么,你得看下這段程序在運行時的狀況如何。我們來打開-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)

從日志中可以看到,少數(shù)幾次的Eden區(qū)的新生代GC過后,JVM開始采用更昂貴的Full GC來清理老生代和持久代的空間。為什么會這樣?既然已經(jīng)沒有人引用這些對象了,為什么它們沒有在新生代中被回收掉?代碼這么寫有什么問題嗎?

要弄清楚GC這個行為的原因,我們先來對代碼做一個小的改動,將finalize()方法的實現(xiàn)先去掉?,F(xiàn)在JVM發(fā)現(xiàn)這個類沒有實現(xiàn)finalize()方法了,于是它切換回了”正常”的模式。再看一眼GC的日志,你只能看到一些廉價的新生代GC在不停的運行。

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

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

那現(xiàn)在Eden區(qū)已經(jīng)滿了,而所有對象又都存在引用,GC沒轍了只能把它們?nèi)截惖絊uvivor區(qū)。更糟糕的是,一旦連Survivor區(qū)也滿了,只能存到老生代里面了。你應(yīng)該還記得,Eden區(qū)使用的是一種”拋棄一切”的清理策略,而老生代的GC則完全不同,它采用的是一種開銷更大的方式。

Finalizer隊列

只有在GC完成后,JVM才會意識到除了Finalizer對象已經(jīng)沒有人引用到我們創(chuàng)建的這些實例了,因此它才會把指向這些對象的Finalizer對象標(biāo)記成可處理的。GC內(nèi)部會把這些Finalizer對象放到j(luò)ava.lang.ref.Finalizer.ReferenceQueue這個特殊的隊列里面。

完成了這些麻煩事之后,我們的應(yīng)用程序才能繼續(xù)往下走。這里有個線程你一定會很感興趣——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線程是個單一職責(zé)的線程。這個線程會不停的循環(huán)等待java.lang.ref.Finalizer.ReferenceQueue中的新增對象。一旦Finalizer線程發(fā)現(xiàn)隊列中出現(xiàn)了新的對象,它會彈出該對象,調(diào)用它的finalize()方法,將該引用從Finalizer類中移除,因此下次GC再執(zhí)行的時候,這個Finalizer實例以及它引用的那個對象就可以回垃圾回收掉了。

現(xiàn)在我們有兩個線程都在不停地循環(huán)。我們的主線程在忙著創(chuàng)建新對象。這些對象都有各自的看門狗也就是Finalizer,而這個Finalizer對象會被添加到一個java.lang.ref.Finalizer.ReferenceQueue中。Finalizer線程會負(fù)責(zé)處理這個隊列,它將所有的對象彈出,然后調(diào)用它們的finalize()方法。

很多時候你可能磁不到內(nèi)存溢出這種情況。finalize()方法的調(diào)用會比你創(chuàng)建新對象要早得多。因此大多數(shù)時候,F(xiàn)inalizer線程能夠趕在下次GC帶來更多的Finalizer對象前清空這個隊列。但我們這個例子當(dāng)中,顯然不是這樣。

為什么會出現(xiàn)溢出?因為Finalizer線程和主線程相比它的優(yōu)先級要低。這意味著分配給它的CPU時間更少,因此它的處理速度沒法趕上新對象創(chuàng)建的速度。這就是問題的根源——對象創(chuàng)建的速度要比Finalizer線程調(diào)用finalize()結(jié)束它們的速度要快,這導(dǎo)致最后堆中所有可用的空間都被耗盡了。結(jié)果就是——我們親愛的小伙伴java.lang.OutOfMemoryError會以不同的身份出現(xiàn)在你面前。

如果你仍然不相信我的話,dump一下堆內(nèi)存,看下它里面有什么。比如說,你可以使用-XX:+HeapDumpOnOutOfMemoryError參數(shù)啟動我們這個小程序,在我的Eclipse中的MAT Dominator Tree中我看到的是下面這張圖:

看到了吧,我這個64M的堆全給Finalizer對象給占滿了。

結(jié)論

回顧一下,F(xiàn)inalizable對象的生命周期和普通對象的行為是完全不同的,列舉如下:

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

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

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java中SynchronousQueue的底層實現(xiàn)原理剖析

    Java中SynchronousQueue的底層實現(xiàn)原理剖析

    BlockingQueue的實現(xiàn)類中,有一種阻塞隊列比較特殊,就是SynchronousQueue(同步移交隊列),隊列長度為0。本文就來剖析一下SynchronousQueue的底層實現(xiàn)原理,感興趣的可以了解一下
    2022-11-11
  • java排查進程占用系統(tǒng)內(nèi)存高方法

    java排查進程占用系統(tǒng)內(nèi)存高方法

    這篇文章主要為大家介紹了java進程占用系統(tǒng)內(nèi)存高排查方法,
    2023-06-06
  • SpringBoot整合SQLite數(shù)據(jù)庫全過程

    SpringBoot整合SQLite數(shù)據(jù)庫全過程

    sqlite是一個很輕量級的數(shù)據(jù)庫,可以滿足日常sql的需求,下面這篇文章主要給大家介紹了關(guān)于SpringBoot整合SQLite數(shù)據(jù)庫的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-03-03
  • Java實現(xiàn)簡單客戶信息管理系統(tǒng)

    Java實現(xiàn)簡單客戶信息管理系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)簡單客戶信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-05-05
  • Java數(shù)據(jù)類型(八種基本數(shù)據(jù)類型+四種引用類型)以及數(shù)據(jù)類型轉(zhuǎn)換

    Java數(shù)據(jù)類型(八種基本數(shù)據(jù)類型+四種引用類型)以及數(shù)據(jù)類型轉(zhuǎn)換

    java中除了基本數(shù)據(jù)類型之外,剩下的都是引用數(shù)據(jù)類型,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)類型(八種基本數(shù)據(jù)類型?+?四種引用類型)以及數(shù)據(jù)類型轉(zhuǎn)換的相關(guān)資料,需要的朋友可以參考下
    2024-04-04
  • SpringBoot?多環(huán)境打包最佳實踐記錄

    SpringBoot?多環(huán)境打包最佳實踐記錄

    SpringBoot通過配置多環(huán)境文件和在打包時指定激活的環(huán)境,實現(xiàn)多環(huán)境打包與部署,本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2024-11-11
  • JAVA swing布局管理器實例解析

    JAVA swing布局管理器實例解析

    這篇文章主要介紹了JAVA swing布局管理器實例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-03-03
  • 關(guān)于Java8的foreach中使用return/break/continue產(chǎn)生的問題

    關(guān)于Java8的foreach中使用return/break/continue產(chǎn)生的問題

    這篇文章主要介紹了關(guān)于Java8的foreach()中使用return/break/continue產(chǎn)生的問題,在使用foreach()處理集合時不能使用break和continue這兩個方法,也就是說不能按照普通的for循環(huán)遍歷集合時那樣根據(jù)條件來中止遍歷,需要的朋友可以參考下
    2023-10-10
  • 從Mybatis-Plus開始認(rèn)識SerializedLambda的詳細(xì)過程

    從Mybatis-Plus開始認(rèn)識SerializedLambda的詳細(xì)過程

    這篇文章主要介紹了從Mybatis-Plus開始認(rèn)識SerializedLambda,本文通過實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2024-07-07
  • java通過AOP實現(xiàn)全局日志打印詳解

    java通過AOP實現(xiàn)全局日志打印詳解

    最近自己一直再看現(xiàn)有微服務(wù)的日志模塊,發(fā)現(xiàn)就是使用AOP來做controller層的日志處理,加上項目在進行架構(gòu)優(yōu)化,這篇文章主要給大家介紹了關(guān)于java通過AOP實現(xiàn)全局日志打印的相關(guān)資料,需要的朋友可以參考下
    2022-01-01

最新評論