亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

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

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

如果JVM發(fā)現(xiàn)某個(gè)類實(shí)現(xiàn)了finalize()方法的話,那么見證奇跡的時(shí)刻到了。我們先來創(chuàng)建一個(gè)實(shí)現(xiàn)了這個(gè)非凡的finalize()方法的類,然后看下這種情況下JVM的處理會有什么不同。

先從一個(gè)簡單的示例程序開始

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() });
                    }
          }
     }
}

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

你覺得這小段代碼的輸出結(jié)果會是怎樣的呢?由于新創(chuàng)建的對象很快就沒人引用了,它們馬上就可以被GC回收掉。因此你可能會認(rèn)為這段程序可以不停的運(yù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é)果并非如此?,F(xiàn)實(shí)的結(jié)果完全不同,在我的Mac OS X的JDK 1.7.0_51上,程序大概在創(chuàng)建了120萬個(gè)對象后就拋出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ā)生了什么,你得看下這段程序在運(yùn)行時(shí)的狀況如何。我們來打開-XX:+PrintGCDetails選項(xiàng)再運(yùn)行一次看看:

[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這個(gè)行為的原因,我們先來對代碼做一個(gè)小的改動,將finalize()方法的實(shí)現(xiàn)先去掉。現(xiàn)在JVM發(fā)現(xiàn)這個(gè)類沒有實(shí)現(xiàn)finalize()方法了,于是它切換回了”正常”的模式。再看一眼GC的日志,你只能看到一些廉價(jià)的新生代GC在不停的運(yùn)行。

因?yàn)樾薷暮蟮倪@段程序中,的確沒有人引用到了新生代的這些剛創(chuàng)建的對象。因此Eden區(qū)很快就被清空掉了,整個(gè)程序可以一直的執(zhí)行下去。

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

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

Finalizer隊(duì)列

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

完成了這些麻煩事之后,我們的應(yīng)用程序才能繼續(xù)往下走。這里有個(gè)線程你一定會很感興趣——Finalizer守護(hù)線程。通過使用jstack進(jìn)行thread dump可以看到這個(gè)線程的信息。

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 —

從上面可以看到有一個(gè)Finalizer守護(hù)線程正在運(yùn)行。Finalizer線程是個(gè)單一職責(zé)的線程。這個(gè)線程會不停的循環(huán)等待java.lang.ref.Finalizer.ReferenceQueue中的新增對象。一旦Finalizer線程發(fā)現(xiàn)隊(duì)列中出現(xiàn)了新的對象,它會彈出該對象,調(diào)用它的finalize()方法,將該引用從Finalizer類中移除,因此下次GC再執(zhí)行的時(shí)候,這個(gè)Finalizer實(shí)例以及它引用的那個(gè)對象就可以回垃圾回收掉了。

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

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

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

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

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

結(jié)論

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

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

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

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

相關(guān)文章

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

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

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

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

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

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

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

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

    這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡單客戶信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    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)境打包最佳實(shí)踐記錄

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

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

    JAVA swing布局管理器實(shí)例解析

    這篇文章主要介紹了JAVA swing布局管理器實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    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()處理集合時(shí)不能使用break和continue這兩個(gè)方法,也就是說不能按照普通的for循環(huán)遍歷集合時(shí)那樣根據(jù)條件來中止遍歷,需要的朋友可以參考下
    2023-10-10
  • 從Mybatis-Plus開始認(rèn)識SerializedLambda的詳細(xì)過程

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

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

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

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

最新評論