Java的Finalizer引發(fā)的內(nèi)存溢出問(wèn)題及解決
Java的Finalizer引發(fā)的內(nèi)存溢出
本文介紹的是Java里一個(gè)內(nèi)建的概念,F(xiàn)inalizer。你可能對(duì)它對(duì)數(shù)家珍,但也可能從未聽(tīng)聞過(guò),這得看你有沒(méi)有花時(shí)間完整地看過(guò)一遍java.lang.Object類了。在java.lang.Object里面就有一個(gè)finalize()的方法。這個(gè)方法的實(shí)現(xiàn)是空的,不過(guò)一旦實(shí)現(xiàn)了這個(gè)方法,就會(huì)觸發(fā)JVM的內(nèi)部行為,威力和危險(xiǎn)并存。
如果JVM發(fā)現(xiàn)某個(gè)類實(shí)現(xiàn)了finalize()方法的話,那么見(jiàn)證奇跡的時(shí)刻到了。我們先來(lái)創(chuàng)建一個(gè)實(shí)現(xiàn)了這個(gè)非凡的finalize()方法的類,然后看下這種情況下JVM的處理會(huì)有什么不同。
先從一個(gè)簡(jiǎn)單的示例程序開(kāi)始
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è)無(wú)限循環(huán)來(lái)創(chuàng)建對(duì)象。它同時(shí)還用了一個(gè)靜態(tài)變量aliveCount來(lái)跟蹤一共創(chuàng)建了多少個(gè)實(shí)例。每創(chuàng)建了一個(gè)新對(duì)象,計(jì)數(shù)器會(huì)加1,一旦GC完成后調(diào)用了finalize()方法,計(jì)數(shù)器會(huì)跟著減1。
你覺(jué)得這小段代碼的輸出結(jié)果會(huì)是怎樣的呢?由于新創(chuàng)建的對(duì)象很快就沒(méi)人引用了,它們馬上就可以被GC回收掉。因此你可能會(huì)認(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萬(wàn)個(gè)對(duì)象后就拋出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í)的狀況如何。我們來(lái)打開(kāi)-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過(guò)后,JVM開(kāi)始采用更昂貴的Full GC來(lái)清理老生代和持久代的空間。為什么會(huì)這樣?既然已經(jīng)沒(méi)有人引用這些對(duì)象了,為什么它們沒(méi)有在新生代中被回收掉?代碼這么寫(xiě)有什么問(wèn)題嗎?
要弄清楚GC這個(gè)行為的原因,我們先來(lái)對(duì)代碼做一個(gè)小的改動(dòng),將finalize()方法的實(shí)現(xiàn)先去掉?,F(xiàn)在JVM發(fā)現(xiàn)這個(gè)類沒(méi)有實(shí)現(xiàn)finalize()方法了,于是它切換回了”正常”的模式。再看一眼GC的日志,你只能看到一些廉價(jià)的新生代GC在不停的運(yùn)行。

因?yàn)樾薷暮蟮倪@段程序中,的確沒(méi)有人引用到了新生代的這些剛創(chuàng)建的對(duì)象。因此Eden區(qū)很快就被清空掉了,整個(gè)程序可以一直的執(zhí)行下去。
另一方面,在早先的那個(gè)例子中情況則有些不同。這些對(duì)象并非沒(méi)人引用 ,JVM會(huì)為每一個(gè)Finalizable對(duì)象創(chuàng)建一個(gè)看門(mén)狗(watchdog)。這是Finalizer類的一個(gè)實(shí)例。而所有的這些看門(mén)狗又會(huì)為Finalizer類所引用。由于存在這么一個(gè)引用鏈,因此整個(gè)的這些對(duì)象都是存活的。
那現(xiàn)在Eden區(qū)已經(jīng)滿了,而所有對(duì)象又都存在引用,GC沒(méi)轍了只能把它們?nèi)截惖絊uvivor區(qū)。更糟糕的是,一旦連Survivor區(qū)也滿了,只能存到老生代里面了。你應(yīng)該還記得,Eden區(qū)使用的是一種”拋棄一切”的清理策略,而老生代的GC則完全不同,它采用的是一種開(kāi)銷(xiāo)更大的方式。
Finalizer隊(duì)列
只有在GC完成后,JVM才會(huì)意識(shí)到除了Finalizer對(duì)象已經(jīng)沒(méi)有人引用到我們創(chuàng)建的這些實(shí)例了,因此它才會(huì)把指向這些對(duì)象的Finalizer對(duì)象標(biāo)記成可處理的。GC內(nèi)部會(huì)把這些Finalizer對(duì)象放到j(luò)ava.lang.ref.Finalizer.ReferenceQueue這個(gè)特殊的隊(duì)列里面。
完成了這些麻煩事之后,我們的應(yīng)用程序才能繼續(xù)往下走。這里有個(gè)線程你一定會(huì)很感興趣——Finalizer守護(hù)線程。通過(guò)使用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ì)不停的循環(huán)等待java.lang.ref.Finalizer.ReferenceQueue中的新增對(duì)象。一旦Finalizer線程發(fā)現(xiàn)隊(duì)列中出現(xiàn)了新的對(duì)象,它會(huì)彈出該對(duì)象,調(diào)用它的finalize()方法,將該引用從Finalizer類中移除,因此下次GC再執(zhí)行的時(shí)候,這個(gè)Finalizer實(shí)例以及它引用的那個(gè)對(duì)象就可以回垃圾回收掉了。
現(xiàn)在我們有兩個(gè)線程都在不停地循環(huán)。我們的主線程在忙著創(chuàng)建新對(duì)象。這些對(duì)象都有各自的看門(mén)狗也就是Finalizer,而這個(gè)Finalizer對(duì)象會(huì)被添加到一個(gè)java.lang.ref.Finalizer.ReferenceQueue中。Finalizer線程會(huì)負(fù)責(zé)處理這個(gè)隊(duì)列,它將所有的對(duì)象彈出,然后調(diào)用它們的finalize()方法。
很多時(shí)候你可能磁不到內(nèi)存溢出這種情況。finalize()方法的調(diào)用會(huì)比你創(chuàng)建新對(duì)象要早得多。因此大多數(shù)時(shí)候,F(xiàn)inalizer線程能夠趕在下次GC帶來(lái)更多的Finalizer對(duì)象前清空這個(gè)隊(duì)列。但我們這個(gè)例子當(dāng)中,顯然不是這樣。
為什么會(huì)出現(xiàn)溢出?因?yàn)镕inalizer線程和主線程相比它的優(yōu)先級(jí)要低。這意味著分配給它的CPU時(shí)間更少,因此它的處理速度沒(méi)法趕上新對(duì)象創(chuàng)建的速度。這就是問(wèn)題的根源——對(duì)象創(chuàng)建的速度要比Finalizer線程調(diào)用finalize()結(jié)束它們的速度要快,這導(dǎo)致最后堆中所有可用的空間都被耗盡了。結(jié)果就是——我們親愛(ài)的小伙伴java.lang.OutOfMemoryError會(huì)以不同的身份出現(xiàn)在你面前。
如果你仍然不相信我的話,dump一下堆內(nèi)存,看下它里面有什么。比如說(shuō),你可以使用-XX:+HeapDumpOnOutOfMemoryError參數(shù)啟動(dòng)我們這個(gè)小程序,在我的Eclipse中的MAT Dominator Tree中我看到的是下面這張圖:

看到了吧,我這個(gè)64M的堆全給Finalizer對(duì)象給占滿了。
結(jié)論
回顧一下,F(xiàn)inalizable對(duì)象的生命周期和普通對(duì)象的行為是完全不同的,列舉如下:
JVM創(chuàng)建Finalizable對(duì)象JVM創(chuàng)建 java.lang.ref.Finalizer實(shí)例,指向剛創(chuàng)建的對(duì)象。java.lang.ref.Finalizer類持有新創(chuàng)建的java.lang.ref.Finalizer的實(shí)例。這使得下一次新生代GC無(wú)法回收這些對(duì)象。新生代GC無(wú)法清空Eden區(qū),因此會(huì)將這些對(duì)象移到Survivor區(qū)或者老生代。垃圾回收器發(fā)現(xiàn)這些對(duì)象實(shí)現(xiàn)了finalize()方法。因?yàn)闀?huì)把它們添加到j(luò)ava.lang.ref.Finalizer.ReferenceQueue隊(duì)列中。Finalizer線程會(huì)處理這個(gè)隊(duì)列,將里面的對(duì)象逐個(gè)彈出,并調(diào)用它們的finalize()方法。finalize()方法調(diào)用完后,F(xiàn)inalizer線程會(huì)將引用從Finalizer類中去掉,因此在下一輪GC中,這些對(duì)象就可以被回收了。Finalizer線程會(huì)和我們的主線程進(jìn)行競(jìng)爭(zhēng),不過(guò)由于它的優(yōu)先級(jí)較低,獲取到的CPU時(shí)間較少,因此它永遠(yuǎn)也趕不上主線程的步伐。程序消耗了所有的可用資源,最后拋出OutOfMemoryError異常。
這篇文章想告訴我們什么?下回如果你考慮使用finalize()方法,而不是使用常規(guī)的方式來(lái)清理對(duì)象的話,最好多想一下。你可能會(huì)為使用了finalize()方法寫(xiě)出的整潔的代碼而沾沾自喜,但是不停增長(zhǎng)的Finalizer隊(duì)列也許會(huì)撐爆你的年老代,你需要重新再考慮一下你的方案。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java之Spring認(rèn)證使用Profile配置運(yùn)行環(huán)境講解
這篇文章主要介紹了Java之Spring認(rèn)證使用Profile配置運(yùn)行環(huán)境講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
MyBatis-Plus實(shí)現(xiàn)多表聯(lián)查方式(一對(duì)一,一對(duì)多使用)
文章介紹了MyBatis-Plus-Join插件,一個(gè)在MyBatis-Plus基礎(chǔ)上增強(qiáng)的工具,支持聯(lián)表查詢而不改變?cè)写a結(jié)構(gòu),MPJ插件無(wú)侵入性,兼容MyBatis-Plus的多種特性,并支持注解形式的一對(duì)一和一對(duì)多查詢2025-10-10
Java調(diào)用基于Ollama本地大模型的實(shí)現(xiàn)
本文主要介紹了Java調(diào)用基于Ollama本地大模型的實(shí)現(xiàn),實(shí)現(xiàn)文本生成、問(wèn)答、文本分類等功能,開(kāi)發(fā)者可以輕松配置和調(diào)用模型,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Java創(chuàng)建多線程的幾種方式實(shí)現(xiàn)
這篇文章主要介紹了Java創(chuàng)建多線程的幾種方式實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
Java數(shù)據(jù)結(jié)構(gòu)通關(guān)時(shí)間復(fù)雜度和空間復(fù)雜度
對(duì)于一個(gè)算法,其時(shí)間復(fù)雜度和空間復(fù)雜度往往是相互影響的,當(dāng)追求一個(gè)較好的時(shí)間復(fù)雜度時(shí),可能會(huì)使空間復(fù)雜度的性能變差,即可能導(dǎo)致占用較多的存儲(chǔ)空間,這篇文章主要給大家介紹了關(guān)于Java時(shí)間復(fù)雜度、空間復(fù)雜度的相關(guān)資料,需要的朋友可以參考下2022-05-05
Data Source與數(shù)據(jù)庫(kù)連接池簡(jiǎn)介(JDBC簡(jiǎn)介)
DataSource是作為DriverManager的替代品而推出的,DataSource 對(duì)象是獲取連接的首選方法,這篇文章主要介紹了Data Source與數(shù)據(jù)庫(kù)連接池簡(jiǎn)介(JDBC簡(jiǎn)介),需要的朋友可以參考下2022-11-11
Java編程實(shí)現(xiàn)對(duì)十六進(jìn)制字符串異或運(yùn)算代碼示例
這篇文章主要介紹了Java編程實(shí)現(xiàn)對(duì)十六進(jìn)制字符串異或運(yùn)算代碼示例,簡(jiǎn)述了異或運(yùn)算以及具體實(shí)例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12

