自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入解析Java OutOfMemoryError

開(kāi)發(fā) 后端
在Java中,所有對(duì)象都存儲(chǔ)在堆中。他們通過(guò)new關(guān)鍵字來(lái)進(jìn)行分配,JVM會(huì)檢查是否所有線程都無(wú)法在訪問(wèn)他們了,并且會(huì)將他們進(jìn)行回收。在大多數(shù)時(shí)候程序員都不會(huì)有一絲一毫的察覺(jué),這些工作都被靜悄悄的執(zhí)行。但是,有時(shí)候在發(fā)布前的最后一天,程序掛了。

在Java中,所有對(duì)象都存儲(chǔ)在堆中。他們通過(guò)new關(guān)鍵字來(lái)進(jìn)行分配,JVM會(huì)檢查是否所有線程都無(wú)法在訪問(wèn)他們了,并且會(huì)將他們進(jìn)行回收。在大多數(shù)時(shí)候程序員都不會(huì)有一絲一毫的察覺(jué),這些工作都被靜悄悄的執(zhí)行。但是,有時(shí)候在發(fā)布前的***一天,程序掛了。

  1. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 

OutOfMemoryError是一個(gè)讓人很郁悶的異常。它通常說(shuō)明你干了寫(xiě)錯(cuò)誤的事情:沒(méi)必要的長(zhǎng)時(shí)間保存一些沒(méi)必要的數(shù)據(jù),或者同一時(shí)間處理了過(guò)多的數(shù)據(jù)。有些時(shí)候,這些問(wèn)題并不一定受你的控制,比如說(shuō)一些第三方的庫(kù)對(duì)一些字符串做了緩存,或者一些應(yīng)用服務(wù)器在部署的時(shí)候并沒(méi)有進(jìn)行清理。并且,對(duì)于堆中已經(jīng)存在的對(duì)象,我們往往拿他們沒(méi)辦法。

這篇文章分析了導(dǎo)致OutOfMemoryError的不同原因,以及你該怎樣應(yīng)對(duì)這種原因的方法。以下分析僅限于Sun Hotspot虛擬機(jī),但是大多數(shù)結(jié)論都適用于其他任何的JVM實(shí)現(xiàn)。它們大多數(shù)基于網(wǎng)上的文章以及我自己的經(jīng)驗(yàn)。我沒(méi)有直接做JVM開(kāi)發(fā)的工作,因此結(jié)論并不代表JVM的作者。但是我確實(shí)曾經(jīng)遇到過(guò)并解決了很多內(nèi)存相關(guān)的問(wèn)題。

垃圾回收介紹

我在這篇文章中已經(jīng)詳細(xì)介紹了垃圾回收的過(guò)程。簡(jiǎn)單的說(shuō),標(biāo)記-清除算法(mark-sweep collect)以garbage collection roots作為掃描的起點(diǎn),并對(duì)整個(gè)對(duì)象圖進(jìn)行掃描,對(duì)所有可達(dá)的對(duì)象進(jìn)行標(biāo)記。那些沒(méi)有被標(biāo)記的對(duì)象會(huì)被清除并回收。

Java的垃圾回收算法過(guò)程意味著如果出現(xiàn)了OOM,那么說(shuō)明你在不停的往對(duì)象圖中添加對(duì)象并且沒(méi)有移除它們。這通常是因?yàn)槟阍谕粋€(gè)集合類(lèi)中添加了很多對(duì)象,比如Map,并且這個(gè)集合對(duì)象是static的?;蛘撸@個(gè)集合類(lèi)被保存在了ThreadLocal對(duì)象中,而這個(gè)對(duì)應(yīng)的Thread卻又長(zhǎng)時(shí)間的運(yùn)行,一直不退出。

這與C和C++的內(nèi)存泄露完全不一樣。在這些語(yǔ)言中,如果一些方法調(diào)用了malloc()或者new,并且在方法退出的時(shí)候沒(méi)有調(diào)用相應(yīng)的free()或者delete,那么內(nèi)存就會(huì)產(chǎn)生泄露。這些是真正意義上得泄露,你在這個(gè)進(jìn)程范圍內(nèi)不可能再恢復(fù)這些內(nèi)存,除非使用一些特定的工具來(lái)保證每一個(gè)內(nèi)存分配方法都有其對(duì)應(yīng)的內(nèi)存釋放操作相對(duì)應(yīng)。

在java中,“泄露”這個(gè)詞往往被誤用了。因?yàn)閺腏VM的角度來(lái)說(shuō),所有的內(nèi)存都是被良好管理的。問(wèn)題僅僅是作為程序員的你不知道這些內(nèi)存是被哪些對(duì)象占用了。但是幸運(yùn)的是,你還是有辦法去找到和定位它們。

在深入探討之前,你還有***一件關(guān)于垃圾收集的知識(shí)需要了解:JVM會(huì)盡***的能力去釋放內(nèi)存,直到發(fā)生OOM。這就意味著OOM不能通過(guò)簡(jiǎn)單的調(diào)用System.gc()來(lái)解決,你需要找到這些“泄露”點(diǎn),并自己處理它們。

設(shè)置堆大小

學(xué)院派的人非常喜歡說(shuō)Java語(yǔ)言規(guī)范并沒(méi)有對(duì)垃圾收集器進(jìn)行任何約定,你甚至可以實(shí)現(xiàn)一個(gè)從來(lái)不釋放內(nèi)存的JVM(實(shí)際是毫無(wú)意義的)。Java虛擬機(jī)規(guī)范中提到堆是由垃圾回收器進(jìn)行管理,但是卻沒(méi)有說(shuō)明任何相關(guān)細(xì)節(jié)。僅僅說(shuō)了我剛才提到的那句話(huà):垃圾回收會(huì)發(fā)生在OOM之前。

實(shí)際上,Sun Hotspot虛擬機(jī)使用了一個(gè)固定大小的堆空間,并且允許在最小空間和***空間之間進(jìn)行自動(dòng)增長(zhǎng)。如果你沒(méi)有指定最小值和***值,那么對(duì)于’client’模式將會(huì)默認(rèn)使用2Mb最為最小值,64Mb最為***值;對(duì)于’server’模式,JVM會(huì)根據(jù)當(dāng)前可用內(nèi)存來(lái)決定默認(rèn)值。2000年后,默認(rèn)的***堆大小改為了64M,并且在當(dāng)時(shí)已經(jīng)認(rèn)為足夠大了(2000年前的時(shí)候默認(rèn)值是16M),但是對(duì)于現(xiàn)在的應(yīng)用程序來(lái)說(shuō)很容易就用完了。

這意味著你需要顯示的通過(guò)JVM參數(shù)來(lái)指定堆的最小值和***值:

  1. java -Xms256m -Xmx512m MyClass 

這里有很多經(jīng)驗(yàn)上得法則來(lái)設(shè)定***值和最小值。顯然,堆的***值應(yīng)該設(shè)定為足以容下整個(gè)應(yīng)用程序所需要的全部對(duì)象。但是,將它設(shè)定為“剛剛好足夠大”也不是一個(gè)很好的注意,因?yàn)檫@樣會(huì)增加垃圾回收器的負(fù)載。因此,對(duì)于一個(gè)長(zhǎng)時(shí)間運(yùn)行的應(yīng)用程序,你一般需要保持有20%-25%的空閑堆空間。(你得應(yīng)用程序可能需要不同的參數(shù)設(shè)置,GC調(diào)優(yōu)是一門(mén)藝術(shù),并且不在該文章討論范圍內(nèi))

讓你奇怪的時(shí),設(shè)置合適的堆的最小值往往比設(shè)置合適的***值更加重要。垃圾回收器會(huì)盡可能的保證當(dāng)前的的堆大小,而不是不停的增長(zhǎng)堆空間。這會(huì)導(dǎo)致應(yīng)用程序不停的創(chuàng)建和回收大量的對(duì)象,而不是獲取新的堆空間,相對(duì)于初始(最小)堆空間。Java堆會(huì)盡量保持這樣的堆大小,并且會(huì)不停的運(yùn)行GC以保持這樣的容量。因此,我認(rèn)為在生產(chǎn)環(huán)境中,我們***是將堆的最小值和***值設(shè)置成一樣的。

你可能會(huì)困惑于為什么Java堆會(huì)有一個(gè)***值上限:操作系統(tǒng)并不會(huì)分配真正的物理內(nèi)存,除非他們真的被使用了。并且,實(shí)際使用的虛擬內(nèi)存空間實(shí)際上會(huì)比Java堆空間要大。如果你運(yùn)行在一個(gè)32位系統(tǒng)上,一個(gè)過(guò)大的堆空間可能會(huì)限制classpath中能夠使用的jar的數(shù)量,或者你可以創(chuàng)建的線程數(shù)。

另外一個(gè)原因是,一個(gè)受限的***堆空間可以讓你及時(shí)發(fā)現(xiàn)潛在的內(nèi)存泄露問(wèn)題。在開(kāi)發(fā)環(huán)境中,對(duì)應(yīng)用程序的壓力往往是不夠的,如果你在開(kāi)發(fā)環(huán)境中就擁有一個(gè)非常大得堆空間,那么你很有可能永遠(yuǎn)不會(huì)發(fā)現(xiàn)可能的內(nèi)存泄露問(wèn)題,直到進(jìn)入產(chǎn)品環(huán)境。

在運(yùn)行時(shí)跟蹤垃圾回收

所有的JVM實(shí)現(xiàn)都提供了-verbos:gc選項(xiàng),它可以讓垃圾回收器在工作的時(shí)候打印出日志信息:

 

  1. java -verbose:gc com.kdgregory.example.memory.SimpleAllocator  
  2. [GC 1201K->1127K(1984K), 0.0020460 secs]  
  3. [Full GC 1127K->103K(1984K), 0.0196060 secs]  
  4. [GC 1127K->1127K(1984K), 0.0006680 secs]  
  5. [Full GC 1127K->103K(1984K), 0.0180800 secs]  
  6. [GC 1127K->1127K(1984K), 0.0001970 secs]  
  7. ... 

Sun的JVM提供了額外的兩個(gè)參數(shù)來(lái)以?xún)?nèi)存帶分類(lèi)輸出,并且會(huì)顯示垃圾收集的開(kāi)始時(shí)間:

  1. java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps com.kdgregory.example.memory.SimpleAllocator 
  2. 0.095: [GC 0.095: [DefNew: 177K->64K(576K), 0.0020030 secs]0.097: [Tenured: 1063K->103K(1408K), 0.0178500 secs] 1201K->103K(1984K), 0.0201140 secs] 
  3. 0.117: [GC 0.118: [DefNew: 0K->0K(576K), 0.0007670 secs]0.119: [Tenured: 1127K->103K(1408K), 0.0392040 secs] 1127K->103K(1984K), 0.0405130 secs] 
  4. 0.164: [GC 0.164: [DefNew: 0K->0K(576K), 0.0001990 secs]0.164: [Tenured: 1127K->103K(1408K), 0.0173230 secs] 1127K->103K(1984K), 0.0177670 secs] 
  5. 0.183: [GC 0.184: [DefNew: 0K->0K(576K), 0.0003400 secs]0.184: [Tenured: 1127K->103K(1408K), 0.0332370 secs] 1127K->103K(1984K), 0.0342840 secs] 
  6. ... 

從上面的輸出我們可以看出什么?首先,前面的幾次垃圾回收發(fā)生的非常頻繁。每行的***個(gè)字段顯示了JVM啟動(dòng)后的時(shí)間,我們可以看到在一秒鐘內(nèi)有上百次的GC。并且,還加入了每次GC執(zhí)行時(shí)間的開(kāi)始時(shí)間(在每行的***一個(gè)字段),可以看出垃圾搜集器是在不停的運(yùn)行的。

但是在實(shí)時(shí)系統(tǒng)中,這會(huì)造成很大的問(wèn)題,因?yàn)槔鸭鞯膱?zhí)行會(huì)奪走很多的CPU周期。就像我之前提到的,這很可能是由于初始堆大小設(shè)置的太小了,并且GC日志顯示了:每次堆的大小達(dá)到了1.1Mb,它就開(kāi)始執(zhí)行GC。如果你得系統(tǒng)也有類(lèi)似的現(xiàn)象,請(qǐng)?jiān)诟淖冏约旱膽?yīng)用程序之前使用-Xms來(lái)增大初始堆大小。

對(duì)于GC日志還有一些很有趣的地方:除了***次垃圾回收,沒(méi)有任何對(duì)象是存放在了新生代(“DefNew”)。這說(shuō)明了這個(gè)應(yīng)用程序分配了包含大量數(shù)據(jù)的數(shù)組,在顯示世界里這是很少出現(xiàn)的。如果在一個(gè)實(shí)時(shí)系統(tǒng)中出現(xiàn)這樣的狀況,我想到的***個(gè)問(wèn)題是“這些數(shù)組拿來(lái)干什么用?”。

堆轉(zhuǎn)儲(chǔ)(Heap Dumps)

一個(gè)堆轉(zhuǎn)儲(chǔ)可以顯示你在應(yīng)用程序說(shuō)使用的所有對(duì)象。從基礎(chǔ)上講,它僅僅反映了對(duì)象實(shí)例的數(shù)量和類(lèi)文件所占用的字節(jié)數(shù)。當(dāng)然你也可以將分配這些內(nèi)存的代碼一起dump出來(lái),并且對(duì)比歷史存貨對(duì)象。但是,如果你要dump的數(shù)據(jù)信息越多,JVM的負(fù)載就會(huì)越大,因此這些技術(shù)僅僅應(yīng)該使用在開(kāi)發(fā)環(huán)境中。

怎樣獲得一個(gè)內(nèi)存轉(zhuǎn)儲(chǔ)

命令行參數(shù)-XX:+HeapDumpOnOutOfMemoryError是最簡(jiǎn)單的方式生成內(nèi)存轉(zhuǎn)儲(chǔ)。就像它的名字所說(shuō)的,它會(huì)在內(nèi)存被用完的時(shí)候(發(fā)生OOM)進(jìn)行轉(zhuǎn)儲(chǔ),這在產(chǎn)品環(huán)境非常好用。但是由于這個(gè)是一種事后轉(zhuǎn)儲(chǔ)(已經(jīng)發(fā)生了OOM),它只能提供一種歷史性的數(shù)據(jù)。它會(huì)產(chǎn)生一個(gè)二進(jìn)制文件,你可以使用jhat來(lái)操作該文件(這個(gè)工具在JDK1.6中已經(jīng)提供,但是可以讀取JDK1.5產(chǎn)生的文件)。

你可以使用jmap(JDK1.5之后就自帶了)來(lái)為一個(gè)運(yùn)行中得java程序產(chǎn)生堆轉(zhuǎn)儲(chǔ),可以產(chǎn)生一個(gè)在jhat中使用的dump文件,或者是一個(gè)存文本的統(tǒng)計(jì)文件。統(tǒng)計(jì)圖可以在進(jìn)行分析時(shí)優(yōu)先使用,特別是你要在一段時(shí)間內(nèi)多次轉(zhuǎn)儲(chǔ)堆并進(jìn)行分析和對(duì)比歷史數(shù)據(jù)。

從轉(zhuǎn)儲(chǔ)內(nèi)容和JVM的負(fù)荷的擴(kuò)展性上考慮的話(huà),可以使用profilers。Profiles使用JVM的調(diào)試接口(debuging interface)來(lái)搜集對(duì)象的內(nèi)存分配信息,包括具體的代碼行和方法調(diào)用棧。這個(gè)是非常有用的:不僅僅可以知道你分配了一個(gè)數(shù)GB的數(shù)組,你還可以知道你在一個(gè)特定的地方分配了950MB的對(duì)象,并且直接忽略其他的對(duì)象。當(dāng)然,這些結(jié)果肯定會(huì)對(duì)JVM有開(kāi)銷(xiāo),包括CPU的開(kāi)銷(xiāo)和內(nèi)存的開(kāi)銷(xiāo)(保存一些原始數(shù)據(jù))。你不應(yīng)該在產(chǎn)品環(huán)境中使用profiles。

堆轉(zhuǎn)儲(chǔ)分析:live objects

Java中的內(nèi)存泄露是這樣定義的:你在內(nèi)存中分配了一些對(duì)象,但是并沒(méi)有清除掉所有對(duì)它們的引用,也就是說(shuō)垃圾搜集器不能回收它們。使用堆轉(zhuǎn)儲(chǔ)直方圖可以很容易的查找這些泄露對(duì)象:它不僅僅可以告訴你在內(nèi)存中分配了哪些對(duì)象,并且顯示了這些對(duì)象在內(nèi)存中所占用的大小。但是這種直方圖***的問(wèn)題是:對(duì)于同一個(gè)類(lèi)的所有對(duì)象都被聚合(group)在一起了,所以你還需要進(jìn)一步做一些檢測(cè)來(lái)確定這些內(nèi)存在哪里被分配了。

使用jmap并且加上-histo參數(shù)可以為你產(chǎn)生一個(gè)直方圖,它顯示了從程序運(yùn)行到現(xiàn)在所有對(duì)象的數(shù)量和內(nèi)存消耗,并且包含了已經(jīng)被回收的對(duì)象和內(nèi)存。如果使用-histo:live參數(shù)會(huì)顯示當(dāng)前還在堆中得對(duì)象數(shù)量及其內(nèi)存消耗,不論這些對(duì)象是否要被垃圾搜集器進(jìn)行回收。

也就是說(shuō),如果你要得到一個(gè)當(dāng)前時(shí)間下得準(zhǔn)確信息,你需要在使用jmap之前強(qiáng)制執(zhí)行一次垃圾回收。如果你的應(yīng)用程序是運(yùn)行在本地,最簡(jiǎn)單的方式是直接使用jconsole:在’Memory’標(biāo)簽下,有一個(gè)’Perform GC’的按鈕。如果應(yīng)用程序是運(yùn)行在服務(wù)端環(huán)境,并且JMX beans被暴露了,MemoryMXBean有一個(gè)gc()操作。如果上述的兩種方案都沒(méi)辦法滿(mǎn)足你得要求,你就只有等待JVM自己觸發(fā)一次垃圾搜集過(guò)程了。如果你有一個(gè)很?chē)?yán)重的內(nèi)存泄露問(wèn)題,那么***次major collection很可能預(yù)示著不久后就會(huì)OOM。

有兩種方法使用jmap產(chǎn)生的直方圖。其中最有效的方法,適用于長(zhǎng)時(shí)間運(yùn)行的程序,可以使用帶live的命令行參數(shù),并且在一段時(shí)間內(nèi)多次使用該命令,檢查哪些對(duì)象的數(shù)量在不斷增長(zhǎng)。但是,根據(jù)當(dāng)前程序的負(fù)載,該過(guò)程可能會(huì)花費(fèi)1個(gè)小時(shí)或者更多的時(shí)間。

另外一個(gè)更加快速的方式是直接比較當(dāng)前存活的對(duì)象數(shù)量和總的對(duì)象數(shù)量。如果有些對(duì)象占據(jù)了總對(duì)象數(shù)量的大部分,那么這些對(duì)象很有可能發(fā)生內(nèi)存泄露。這里有一個(gè)例子,這個(gè)應(yīng)用程序已經(jīng)連續(xù)幾周為100多個(gè)用戶(hù)提供了服務(wù),結(jié)果列舉了前12個(gè)數(shù)量最多的對(duì)象。據(jù)我所知,這個(gè)程序沒(méi)有內(nèi)存泄露的問(wèn)題,但是像其他應(yīng)用程序一樣做了常規(guī)性的內(nèi)存轉(zhuǎn)儲(chǔ)分析操作。

 

  1. ~, 510> jmap -histo 7626 | more  
  2. num #instances #bytes class name  
  3. ----------------------------------------------  
  4. 1: 339186 63440816 [C  
  5. 2: 84847 18748496 [I  
  6. 3: 69678 15370640 [Ljava.util.HashMap$Entry;  
  7. 4: 381901 15276040 java.lang.String  
  8. 5: 30508 13137904 [B  
  9. 6: 182713 10231928 java.lang.ThreadLocal$ThreadLocalMap$Entry  
  10. 7: 63450 8789976  
  11. 8: 181133 8694384 java.lang.ref.WeakReference  
  12. 9: 43675 7651848 [Ljava.lang.Object;  
  13. 10: 63450 7621520  
  14. 11: 6729 7040104  
  15. 12: 134146 6439008 java.util.HashMap$Entry  
  16. ~, 511> jmap -histo:live 7626 | more  
  17. num #instances #bytes class name  
  18. ----------------------------------------------  
  19. 1: 200381 35692400 [C  
  20. 2: 22804 12168040 [I  
  21. 3: 15673 10506504 [Ljava.util.HashMap$Entry;  
  22. 4: 17959 9848496 [B  
  23. 5: 63208 8766744  
  24. 6: 199878 7995120 java.lang.String  
  25. 7: 63208 7592480  
  26. 8: 6608 6920072  
  27. 9: 93830 5254480 java.lang.ThreadLocal$ThreadLocalMap$Entry  
  28. 10: 107128 5142144 java.lang.ref.WeakReference  
  29. 11: 93462 5135952  
  30. 12: 6608 4880592 

當(dāng)我們要嘗試尋找內(nèi)存泄露問(wèn)題,可以從消耗內(nèi)存最多的對(duì)象著手。這聽(tīng)上去很明顯,但是往往它們并不是內(nèi)存泄露的根源。但是,它們?nèi)稳皇菓?yīng)該***下手的地方,在這個(gè)例子中,最占用內(nèi)存的是一些char[]的數(shù)組對(duì)象(總大小是60MB,基本上沒(méi)有任何問(wèn)題)。但是很奇怪的是當(dāng)前存貨(live)的對(duì)象竟然占了歷史分配的總對(duì)象大小的三分之二。

一般來(lái)說(shuō),一個(gè)應(yīng)用程序會(huì)分配對(duì)象,并且在不久之后就會(huì)釋放它們。如果保存一些對(duì)象的應(yīng)用過(guò)長(zhǎng)的時(shí)間,就很有可能會(huì)導(dǎo)致內(nèi)存泄露。但是雖然是這么說(shuō)的,實(shí)際上還是要具體情況具體分析,主要還是要看這個(gè)程序到底在做什么事情。字符數(shù)組對(duì)象(char[])往往和字符串對(duì)象(String)同時(shí)存在,大部分的應(yīng)用程序都會(huì)在整個(gè)運(yùn)行過(guò)程中一直保持著一些字符串對(duì)象的引用。例如,基于JSP的web應(yīng)用程序在JSP頁(yè)面中定義了很多HTML字符串表達(dá)式。這種特殊的應(yīng)用程序提供HTML服務(wù),但是它們需要保持字符串引用的需求卻不一定那么清晰:它們提供的是目錄服務(wù),并不是靜態(tài)文本。如果我遇到了OOM,我就會(huì)嘗試找到這些字符串在哪里被分配,為什么沒(méi)有被釋放。

另一個(gè)需要關(guān)注的是字節(jié)數(shù)組([B)。在JDK中有很多類(lèi)都會(huì)使用它們(比如BufferedInputStream),但是卻很少在應(yīng)用程序代碼中直接看到它們。通常它們會(huì)被用作緩存(buffer),但是緩存的生命周期不會(huì)很長(zhǎng)。在這個(gè)例子中我們看到,有一半的字節(jié)數(shù)組任然保持存活。這個(gè)是令人擔(dān)憂(yōu)的,并且它凸顯了直方圖的一個(gè)問(wèn)題:所有的對(duì)象都按照它的類(lèi)型被分組聚合了。對(duì)于應(yīng)用程序?qū)ο?非JDK類(lèi)型或者原始類(lèi)型,在應(yīng)用程序代碼中定義的類(lèi)),這不是一個(gè)問(wèn)題,因?yàn)樗鼈儠?huì)在程序的一個(gè)部分被集中分配。但是字節(jié)數(shù)組有可能會(huì)在任何地方被定義,并且在大多數(shù)應(yīng)用程序中都被隱藏在一些庫(kù)中。我們是否應(yīng)當(dāng)搜索調(diào)用了new byte[]或者new ByteArrayOutputStream()的代碼?

堆轉(zhuǎn)儲(chǔ)分析:相關(guān)的原因和影響分析

為了找到導(dǎo)致內(nèi)存泄露的最終原因,僅僅考慮按照類(lèi)別(class)的分組的內(nèi)存占用字節(jié)數(shù)是不夠的。你還需要將應(yīng)用程序分配的對(duì)象和內(nèi)存泄露的對(duì)象關(guān)聯(lián)起來(lái)考慮。一個(gè)方法是更加深入查看對(duì)象的數(shù)量,以便將具有關(guān)聯(lián)性的對(duì)象找出來(lái)。下面是一個(gè)具有嚴(yán)重內(nèi)存問(wèn)題的程序的轉(zhuǎn)儲(chǔ)信息:

 

  1. num #instances #bytes class name  
  2. ----------------------------------------------  
  3. 1: 1362278 140032936 [Ljava.lang.Object;  
  4. 2: 12624 135469922 [B  
  5. ...  
  6. 5: 352166 45077248 com.example.ItemDetails  
  7. ...  
  8. 9: 1360742 21771872 java.util.ArrayList  
  9. ...  
  10. 41: 6254 200128 java.net.DatagramPacket 

如果你僅僅去看信息的前幾行,你可能會(huì)去定位Object[]或者byte[],這些都是徒勞的。真正的問(wèn)題出在ItemDetails和DatagramPacket上:前者分配了大量的ArrayList,進(jìn)而又分配了大量的Object[];后者使用了大量的byte[]來(lái)保存從網(wǎng)絡(luò)上接收到的數(shù)據(jù)。

***個(gè)問(wèn)題,分配了大量的數(shù)組,實(shí)際上不是內(nèi)存泄露。ArrayList的默認(rèn)構(gòu)造函數(shù)會(huì)分配容量是10的數(shù)組,但是程序本身一般只使用1個(gè)或者2個(gè)槽位,這對(duì)于64位JVM來(lái)說(shuō)會(huì)浪費(fèi)62個(gè)字節(jié)的內(nèi)存空間。一個(gè)更好的涉及方案是僅僅在有需要的時(shí)候才使用List,這樣對(duì)每個(gè)實(shí)例來(lái)說(shuō)可以節(jié)約額外的48個(gè)字節(jié)。但是,對(duì)于這種問(wèn)題也可以很輕易的通過(guò)加內(nèi)存來(lái)解決,因?yàn)楝F(xiàn)在的內(nèi)存非常便宜。

但是對(duì)于datagram的泄露就比較麻煩(如同定位這個(gè)問(wèn)題一樣困難):這表明接收到的數(shù)據(jù)沒(méi)有被盡快的處理掉。

為了跟蹤問(wèn)題的原因和影響,你需要知道你的程序是怎樣在使用這些對(duì)象。不多的程序才會(huì)直接使用Object[]:如果確實(shí)要使用數(shù)組,程序員一般都會(huì)使用帶類(lèi)型的數(shù)組。但是,ArrayList會(huì)在內(nèi)部使用。但是僅僅知道ArrayList的內(nèi)存分配是不夠的,你還需要順著調(diào)用鏈往上走,看看誰(shuí)分配了這些ArrayList。

其中一個(gè)方法是對(duì)比相關(guān)的對(duì)象數(shù)量。在上面的例子中,byte[]和DatagramPackage的關(guān)系是很明顯的:其中一個(gè)基本上是另外一個(gè)的兩倍。但是ArrayList和ItemDetails的關(guān)系就不那么明顯了。(實(shí)際上一個(gè)ItemDetails中會(huì)包含多個(gè)ArrayList)

這往往是個(gè)陷阱,讓你去關(guān)注那么數(shù)量最多的一些對(duì)象。我們有數(shù)百萬(wàn)的ArrayList對(duì)象,并且它們分布在不同的class中,也有可能集中在一小部分class中。盡管如此,數(shù)百萬(wàn)的對(duì)象引用是很容易被定位的。就算有10來(lái)個(gè)class可能會(huì)包含ArrayList,那么每個(gè)class的實(shí)體對(duì)象也會(huì)有十萬(wàn)個(gè),這個(gè)是很容易被定位的。

從直方圖中跟蹤這種引用關(guān)系鏈?zhǔn)切枰ㄙM(fèi)大量精力的,幸運(yùn)的是,jmap不僅僅可以提供直方圖,它還可以提供可以瀏覽的堆轉(zhuǎn)儲(chǔ)信息。

堆轉(zhuǎn)儲(chǔ)分析:跟蹤引用鏈

瀏覽堆轉(zhuǎn)儲(chǔ)引用鏈具有兩個(gè)步驟:首先需要使用-dump參數(shù)來(lái)使用jmap,然后需要用jhat來(lái)使用轉(zhuǎn)儲(chǔ)文件。如果你確定要使用這種方法,請(qǐng)一定要保證有足夠多的內(nèi)存:一個(gè)轉(zhuǎn)儲(chǔ)文件通常都有數(shù)百M(fèi),jhat需要好幾個(gè)G的內(nèi)存來(lái)處理這些轉(zhuǎn)儲(chǔ)文件。

 

  1. tmp, 517> jmap -dump:live,file=heapdump.06180803 7626  
  2. Dumping heap to /home/kgregory/tmp/heapdump.06180803 ...  
  3. Heap dump file created  
  4. tmp, 518> jhat -J-Xmx8192m heapdump.06180803  
  5. Reading from heapdump.06180803...  
  6. Dump file created Sat Jun 18 08:04:22 EDT 2011  
  7. Snapshot read, resolving...  
  8. Resolving 335643 objects...  
  9. Chasing references, expect 67 dots...................................................................  
  10. Eliminating duplicate references...................................................................  
  11. Snapshot resolved.  
  12. Started HTTP server on port 7000  
  13. Server is ready. 

提供給你的默認(rèn)URL顯示了所有加載進(jìn)系統(tǒng)的class,但是我覺(jué)得并不是很有用。相反,我直接使用http://localhost:7000/histo/,這個(gè)地址是一個(gè)直方圖的視角來(lái)進(jìn)行顯示,并且是按照對(duì)象數(shù)量和占用的內(nèi)存空間進(jìn)行排序了的。

這個(gè)直方圖里的每個(gè)class的名稱(chēng)都是一個(gè)鏈接,點(diǎn)擊這個(gè)鏈接可以查看關(guān)于這個(gè)類(lèi)型的詳細(xì)信息。你可以在其中看到這個(gè)類(lèi)的繼承關(guān)系,它的成員變量,以及很多指向這個(gè)類(lèi)的實(shí)體變量信息的鏈接。我不認(rèn)為這個(gè)詳細(xì)信息頁(yè)面非常有用,而且實(shí)體變量的鏈接列表很占用很多的瀏覽器內(nèi)存。

為了能夠跟蹤你的內(nèi)存問(wèn)題,最有用的頁(yè)面是’Reference by Type’。這個(gè)頁(yè)面含有兩個(gè)表格:入引用和出引用,他們都被引用的數(shù)量進(jìn)行排序了。點(diǎn)擊一個(gè)類(lèi)的名字可以看到這個(gè)引用的信息。

你可以在類(lèi)的詳細(xì)信息(class details)頁(yè)面中找到這個(gè)頁(yè)面的鏈接。

堆轉(zhuǎn)儲(chǔ)分析:內(nèi)存分配情況

在大多數(shù)情況下,知道了是哪些對(duì)象消耗了大量的內(nèi)存往往就可以知道它們?yōu)槭裁磿?huì)發(fā)生內(nèi)存泄露。你可以使用jhat來(lái)找到所有引用了他們的對(duì)象,并且你還可以看到使用了這些對(duì)象的引用的代碼。但是在有些時(shí)候,這樣還是不夠的。

比如說(shuō)你有關(guān)于字符串對(duì)象的內(nèi)存泄露問(wèn)題,那么就很有可能會(huì)花費(fèi)你好幾天的時(shí)間去檢查所有和字符串相關(guān)的代碼。要解決這種問(wèn)題,你就需要能夠顯示內(nèi)存在哪里被分配的堆轉(zhuǎn)儲(chǔ)。但是需要注意的是,這種類(lèi)型的堆轉(zhuǎn)儲(chǔ)會(huì)對(duì)你的應(yīng)用程序產(chǎn)生更多的負(fù)載,因?yàn)樨?fù)責(zé)轉(zhuǎn)儲(chǔ)的代理需要記錄每一個(gè)new操作符。

有許多交互式的程序可以做到這種級(jí)別的數(shù)據(jù)記錄,但是我找到了一個(gè)更簡(jiǎn)單的方法,那就是使用內(nèi)置的hprof代理來(lái)啟動(dòng)JVM。

  1. java -Xrunhprof:heap=sites,depth=2 com.kdgregory.example.memory.Gobbler 

hprof有許多選項(xiàng):不僅僅可以用多種方式輸出內(nèi)存使用情況,它還可以跟蹤C(jī)PU的使用情況。當(dāng)它運(yùn)行的時(shí)候,我指定了一個(gè)事后的內(nèi)存轉(zhuǎn)儲(chǔ),它記錄了哪些對(duì)象被分配,以及分配的位置。它的輸出被記錄在了java.hprof.txt文件中,其中關(guān)于堆轉(zhuǎn)儲(chǔ)的部分如下:

 

  1. SITES BEGIN (ordered by live bytes) Tue Sep 29 10:43:34 2009  
  2. percent live alloc'ed stack class  
  3. rank self accum bytes objs bytes objs trace name  
  4. 1 99.77% 99.77% 66497808 2059 66497808 2059 300157 byte[]  
  5. 2 0.01% 99.78% 9192 1 27512 13 300158 java.lang.Object[]  
  6. 3 0.01% 99.80% 8520 1 8520 1 300085 byte[]  
  7. SITES END 

這個(gè)應(yīng)用程序沒(méi)有分配多種不同類(lèi)型的對(duì)象,也沒(méi)有將它們分配到很多不同的地方。一般的轉(zhuǎn)儲(chǔ)有成百上千行的信息,顯示了每一種類(lèi)型的對(duì)象被分配到了哪里。幸運(yùn)的是,大多數(shù)問(wèn)題都會(huì)出現(xiàn)在開(kāi)頭的幾行。在這個(gè)例子中,最突出的是64M的存活著的字節(jié)數(shù)組,并且每一個(gè)平均32K。

大多數(shù)程序中都不會(huì)一直持有這么大得數(shù)據(jù),這就表明這個(gè)程序沒(méi)有很好的抽取和處理這些數(shù)據(jù)。你會(huì)發(fā)現(xiàn)這常常發(fā)生在讀取一些大的字符串,并且保存了substring之后的字符串:很少有人知道String.substring()后會(huì)共享原始字符串對(duì)象的字節(jié)數(shù)組。如果你按照一行一行地讀取了一個(gè)文件,但是卻使用了每行的前五個(gè)字符,實(shí)際上你任然保存的是整個(gè)文件在內(nèi)存中。

轉(zhuǎn)儲(chǔ)文件也顯示出這些數(shù)組被分配的數(shù)量和現(xiàn)在存活的數(shù)量完全相等。這是一種典型的泄露,并且我們可以通過(guò)搜索’trace’號(hào)來(lái)找到真正的代碼:

 

  1. TRACE 300157:  
  2. com.kdgregory.example.memory.Gobbler.main(Gobbler.java:22) 

好了,這下就足夠簡(jiǎn)單了:當(dāng)我在代碼中找到指定的代碼行時(shí),我發(fā)現(xiàn)這些數(shù)組被存放在了ArrayList中,并且它也一直沒(méi)有出作用域。但是有時(shí)候,堆棧的跟蹤并沒(méi)有直接關(guān)聯(lián)到你寫(xiě)的代碼上:

 

  1. TRACE 300085:  
  2. java.util.zip.InflaterInputStream.(InflaterInputStream.java:71)  
  3. java.util.zip.ZipFile$2.(ZipFile.java:348) 

在這個(gè)例子中,你需要增加堆棧跟蹤的深度,并且重新運(yùn)行你的程序。但是這里有一個(gè)需要平衡的地方:當(dāng)你獲取到了更多的堆棧信息,你也同時(shí)增加了profile的負(fù)載。默認(rèn)地,如果你沒(méi)有指定depth參數(shù),那么默認(rèn)值就會(huì)是4。我發(fā)現(xiàn)當(dāng)堆棧深度為2的時(shí)候就可以發(fā)現(xiàn)和定位我程序中得大部分問(wèn)題了,當(dāng)然我也使用過(guò)深度為12的參數(shù)來(lái)運(yùn)行程序。

另外一個(gè)增大堆棧深度的好處是,***的報(bào)告結(jié)果會(huì)更加細(xì)粒度:你可能會(huì)發(fā)現(xiàn)你泄露的對(duì)象來(lái)自?xún)傻饺齻€(gè)地方,并且它們都使用了相同的方法。

堆轉(zhuǎn)儲(chǔ)分析:位置、地點(diǎn)

當(dāng)很多對(duì)象在分配的不久后就被丟棄時(shí),分代垃圾搜集器就會(huì)開(kāi)始運(yùn)行。你可以使用同樣的原則來(lái)找發(fā)現(xiàn)內(nèi)存泄露:使用調(diào)試器,在對(duì)象被分配的地方打上斷點(diǎn),并且運(yùn)行這段代碼。在大多數(shù)時(shí)候,當(dāng)它們被分配不久后就會(huì)加入到長(zhǎng)時(shí)間存活(long-live)的集合中。

***代

除了JVM中的新生代和老年代外,JVM還管理著一片叫‘***代’的區(qū)域,它存儲(chǔ)了class信息和字符串表達(dá)式等對(duì)象。通常,你不會(huì)觀察到***代中的垃圾回收;大多數(shù)的垃圾回收發(fā)生在應(yīng)用程序堆中。但是不像它的名字,在***代中的對(duì)象不會(huì)是***不變的。舉個(gè)例子,被應(yīng)用程序classloader加載的class,當(dāng)不再被classloader引用時(shí)就會(huì)被清理掉。當(dāng)應(yīng)用程序服務(wù)被頻繁的熱部署時(shí)就可能會(huì)發(fā)生:

  1. Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 

這一這個(gè)信息:這個(gè)不管應(yīng)用程序堆的事。當(dāng)應(yīng)用程序堆中還有很多空間時(shí),也有可能用完***代的空間。通常,這發(fā)生在重新部署EAR和WAR文件時(shí),并且***代還不夠大到可以同時(shí)容納新的class信息和老的class信息(老的class會(huì)一直被保存著直到所有的請(qǐng)求在使用完它們)。當(dāng)在運(yùn)行處于開(kāi)發(fā)狀態(tài)的應(yīng)用時(shí)更容易發(fā)生。

解決***代錯(cuò)誤的***個(gè)方法就是增大***大的空間,你可以使用-XX:MaxPermSize命令行參數(shù)。默認(rèn)是64M,但是web應(yīng)用程序或者IDE一般都需要256M。

  1. java -XX:MaxPermSize=256m 

但是在通常情況下并不是這么簡(jiǎn)單的。***代的內(nèi)存泄露一般都和在應(yīng)用堆中的內(nèi)存泄露原因一樣:在一些地方的對(duì)象引用了并不該再引用的對(duì)象。以我的經(jīng)驗(yàn),很有可能有些對(duì)象直接引用了一些Class對(duì)象,或者在java.lang.reflect包下面的對(duì)象,而不是某些類(lèi)的實(shí)例對(duì)象。正式因?yàn)閣eb引用的classloader的組織方式,通常罪魁禍?zhǔn)锥汲霈F(xiàn)在服務(wù)的配置當(dāng)中。

例如,你使用了Tomcat,并且有一個(gè)目錄里面有很多共享的jars:shared/lib。如果你在一個(gè)容器里同時(shí)運(yùn)行好幾個(gè)web應(yīng)用,將一些公用的jar放在這個(gè)目錄是很有道理的,因?yàn)檫@樣的話(huà)這些class僅僅被加載一次,可以減少內(nèi)存的使用量。但是,如果其中的一些庫(kù)具有對(duì)象緩存的話(huà),會(huì)發(fā)生什么事情呢?

答案是這些被緩存了的對(duì)象的類(lèi)永遠(yuǎn)不會(huì)被卸載,直到緩存釋放了這些對(duì)象。解決方案就是將這些庫(kù)移動(dòng)到WAR或者EAR中。但是在某些時(shí)候情況也不會(huì)像這么簡(jiǎn)單:JDKs bean introspector會(huì)緩存住由root classloader加載的BeanInfo對(duì)象。并且任何使用了反射的庫(kù)也會(huì)緩存這些對(duì)象,這樣就導(dǎo)致你不能直到真正的問(wèn)題所在。

解決***代的問(wèn)題通常都是比較痛苦的。一般可以先考慮加上-XX:+TraceClassLoading-XX:+TraceClassUnloading命令行選項(xiàng)以便找出那些被加載了但是沒(méi)有被卸載的類(lèi)。如果你加上了-XX:+TraceClassResolution命令行選項(xiàng),你還可以看到哪些類(lèi)訪問(wèn)了其他類(lèi),但是沒(méi)有被正常卸載。

這里有針對(duì)這三個(gè)選項(xiàng)的一個(gè)實(shí)例。***行顯示了MyClassLoader類(lèi)從classpath中被加載了。因?yàn)樗謴?strong>URLClassLoader繼承,因此我們看到了接下來(lái)的’RESOLVE’消息,緊跟著又是一條’RESOLVE’消息,說(shuō)明Class類(lèi)也被解析了。

 

  1. [Loaded com.kdgregory.example.memory.PermgenExhaustion$MyClassLoader from file:/home/kgregory/Workspace/Website/programming/examples/bin/]  
  2. RESOLVE com.kdgregory.example.memory.PermgenExhaustion$MyClassLoader java.net.URLClassLoader  
  3. RESOLVE java.net.URLClassLoader java.lang.Class URLClassLoader.java:188 

所有的信息都在這里的,但是通常情況下將一些共享庫(kù)移動(dòng)到WAR/EAR中往往可以很快速的解決問(wèn)題。

當(dāng)堆內(nèi)存還有空間時(shí)發(fā)生的OutOfMemoryError

就像你剛才看到的關(guān)于***代的消息,也許應(yīng)用程序堆中還有空閑空間,但是也任然可能會(huì)發(fā)生OOM。這里有幾個(gè)例子:

連續(xù)的內(nèi)存分配

當(dāng)我描述分代的堆空間時(shí),我一般會(huì)說(shuō)對(duì)象會(huì)首先被分配在新生代,然后最終會(huì)被移動(dòng)到老年代。但這不是絕對(duì)正確的:如果你的對(duì)象足夠大,那么它就會(huì)直接被分配在老年代。一般用戶(hù)自己定義的對(duì)象是不會(huì)(也不應(yīng)該)達(dá)到這個(gè)臨界值,但是數(shù)組卻卻有可能:在JDK1.5中,當(dāng)數(shù)組的對(duì)象超過(guò)0.5M的時(shí)候就會(huì)被直接分配到老年代。

在32位機(jī)器上,0.5M換算成Object[]數(shù)組的話(huà)就可以包含131,072個(gè)元素。這已經(jīng)是很大的了,但是在企業(yè)級(jí)的應(yīng)用中這是很有可能的。特別是當(dāng)使用了HashMap時(shí),它經(jīng)常需要重新resize自己(里面的數(shù)組數(shù)據(jù)結(jié)構(gòu))。一些應(yīng)用程序可能還需要更大的數(shù)組。

當(dāng)沒(méi)有連續(xù)的堆空間來(lái)存放這些數(shù)組對(duì)象時(shí)(就算在垃圾回收并且對(duì)內(nèi)存進(jìn)行了緊湊之后),問(wèn)題就產(chǎn)生了。這很少見(jiàn),但是如果當(dāng)前的程序已經(jīng)很接近堆空間的上限時(shí),這就變得很有可能了。增大堆空間上限是***的解決方案,但是你也許可以試試事先分配好你的容器的大小。(后面的小對(duì)象可以不需要連續(xù)的內(nèi)存空間)

線程

JavaDoc中對(duì)OOM的描述是,當(dāng)垃圾搜集器不能在釋放更多的內(nèi)存空間時(shí),JVM會(huì)拋出OOM。這里只對(duì)了一半:當(dāng)JVM的內(nèi)部代碼收到來(lái)自操作系統(tǒng)的ENOMEM錯(cuò)誤時(shí),JVM也會(huì)拋出OOM。Unix程序員一般都知道,這里有很多地方可以收到ENOMEN錯(cuò)誤,創(chuàng)建線程的過(guò)程是其中之一:

  1. Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread 

在我的32位Linux系統(tǒng)中,使用JDK1.5,我可以最多開(kāi)啟5,550個(gè)線程直到拋出異常。但是實(shí)際上在堆中任然有很多空閑空間,這是怎么回事呢?

在這個(gè)場(chǎng)景的背后,線程實(shí)際上是被操作系統(tǒng)所管理,而不是JVM,創(chuàng)建線程失敗的可能原因有很多很多。在我的例子中,每一個(gè)線程都需要占用大概0.5M的虛擬內(nèi)存作為它的??臻g,在5000個(gè)線程被創(chuàng)建之后,大約就有2G的內(nèi)存空間被占用。有些操作系統(tǒng)就強(qiáng)制制定了一個(gè)進(jìn)程所能創(chuàng)建的線程數(shù)的上限。

***,針對(duì)這個(gè)問(wèn)題沒(méi)有一個(gè)解決方案,除非更換你的應(yīng)用程序。大多數(shù)程序是不需要?jiǎng)?chuàng)建這么多得線程的,它們會(huì)將大部分的時(shí)間都浪費(fèi)在等待操作系統(tǒng)調(diào)度上。但是有些服務(wù)程序需要?jiǎng)?chuàng)建數(shù)千個(gè)線程去處理請(qǐng)求,但是它們中得大多數(shù)都是在等待數(shù)據(jù)。針對(duì)這種場(chǎng)景,NIO和selector就是一個(gè)不錯(cuò)的解決方案。

Direct ByteBuffers

從JDK1.4之后Java允許程序程序使用bytebuffers來(lái)訪問(wèn)堆外的內(nèi)存空間(受限)。雖然ByteBuffer對(duì)象本身很小,但是堆外的內(nèi)存可不一定很小:

  1. Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory 

這里有多個(gè)原因會(huì)導(dǎo)致bytebuffer分配失敗。通常情況下,你可能超過(guò)了最多的虛擬內(nèi)存上限(僅限于32位系統(tǒng)),或者超過(guò)了所有物理內(nèi)存和交換區(qū)內(nèi)存的上限。除非你是在以很簡(jiǎn)單的方式處理超過(guò)你的機(jī)器內(nèi)存上限的數(shù)據(jù),否則你在使用direct buffer產(chǎn)生OOM的原因和你使用堆的原因基本上是一樣的:你保持著一些你不該引用的數(shù)據(jù)。前面介紹的堆分析技術(shù)可以幫助你找到泄露點(diǎn)。

申請(qǐng)的內(nèi)存超過(guò)物理內(nèi)存

就像我前面提到的,你在啟動(dòng)一個(gè)JVM時(shí),你需要指定堆的最小值和***值。這就意味著,JVM會(huì)在運(yùn)行期動(dòng)態(tài)改變它對(duì)虛擬內(nèi)存的需求。在一個(gè)內(nèi)存受限的機(jī)器上,你可以同時(shí)運(yùn)行多個(gè)JVM,甚至它們所有指定的***值之和大于了物理內(nèi)存和交換區(qū)的大小。當(dāng)然,這就有可能會(huì)導(dǎo)致OOM,就算你的程序中存活的對(duì)象大小小于你指定的堆空間也是一樣的。

這種情況和跑多個(gè)C++程序使用完所有的物理內(nèi)存的原因是一樣的。使用JVM可能會(huì)讓你產(chǎn)生一種假象,以為不會(huì)出現(xiàn)這種問(wèn)題。唯一的解決方案是購(gòu)買(mǎi)更多的內(nèi)存,或者不要同時(shí)跑那么多程序。沒(méi)有辦法讓JVM可以’快速失敗’;但是在Linux上你可以申請(qǐng)比總內(nèi)存更多的內(nèi)存。

堆外內(nèi)存的使用

***一個(gè)需要注意的問(wèn)題是:Java中得堆僅僅是所占用內(nèi)存的一部分。JVM還會(huì)為它所創(chuàng)建的線程、內(nèi)部代碼、工作空間、共享庫(kù)、direct buffer、內(nèi)存映射文件分配內(nèi)存。在32位的JVM中,這所有的內(nèi)存都需要被映射到2G的虛擬內(nèi)存空間中,這是非常有限的(特別是對(duì)于服務(wù)端或者后端應(yīng)用程序)。在64位的JVM中,虛擬內(nèi)存基本沒(méi)存在什么限制,但是實(shí)際的物理內(nèi)存(含交換區(qū))可能會(huì)很稀缺。

一般來(lái)說(shuō),虛擬內(nèi)存不會(huì)造成什么大問(wèn)題;操作系統(tǒng)和JVM可以很好的管理它們。通常情況下,你需要查看虛擬內(nèi)存的映射情況主要是為了direct buffer所使用的大塊的內(nèi)存或者是內(nèi)存映射文件。但是你還是很有必要知道什么是虛擬內(nèi)存的映射。

要查看在Linux上的虛擬內(nèi)存映射情況可以使用pmap;在Windows中可以使用VMMap。下面是使用pmap來(lái)dump的一個(gè)Tomcat應(yīng)用。實(shí)際的dump文件有好幾百行,所展示的部分僅僅是比較有意思的部分:

 

  1. 08048000 60K r-x-- /usr/local/java/jdk-1.5/bin/java  
  2. 08057000 8K rwx-- /usr/local/java/jdk-1.5/bin/java  
  3. 081e5000 6268K rwx-- [ anon ]  
  4. 889b0000 896K rwx-- [ anon ]  
  5. 88a90000 4096K rwx-- [ anon ]  
  6. 88e90000 10056K rwx-- [ anon ]  
  7. 89862000 50488K rwx-- [ anon ]  
  8. 8c9b0000 9216K rwx-- [ anon ]  
  9. 8d2b0000 56320K rwx-- [ anon ]  
  10. ...  
  11. afd70000 504K rwx-- [ anon ]  
  12. afdee000 12K ----- [ anon ]  
  13. afdf1000 504K rwx-- [ anon ]  
  14. afe6f000 12K ----- [ anon ]  
  15. afe72000 504K rwx-- [ anon ]  
  16. ...  
  17. b0cba000 24K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/catalina-ant-jmx.jar  
  18. b0cc0000 64K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/catalina-storeconfig.jar  
  19. b0cd0000 632K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/catalina.jar  
  20. b0d6e000 164K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/tomcat-ajp.jar  
  21. b0d97000 88K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/tomcat-http.jar  
  22. ...  
  23. b6ee3000 3520K r-x-- /usr/local/java/jdk-1.5/jre/lib/i386/client/libjvm.so  
  24. b7253000 120K rwx-- /usr/local/java/jdk-1.5/jre/lib/i386/client/libjvm.so  
  25. b7271000 4192K rwx-- [ anon ]  
  26. b7689000 1356K r-x-- /lib/tls/i686/cmov/libc-2.11.1.so  
  27. ... 

dump文件展示給你了關(guān)于虛擬內(nèi)存映射的4個(gè)部分:虛擬內(nèi)存地址,大小,權(quán)限,源(從文件加載的部分)。最有意思的部分是它的權(quán)限部分,它表示了該內(nèi)存段是否是只讀的(r-)還是讀寫(xiě)的(rw)。

我會(huì)從讀寫(xiě)段開(kāi)始分析。所有的段都具有名字”[ anon ]“,它在Linux中說(shuō)明了該段不是由文件加載而來(lái)。這里還有很多被命名的讀寫(xiě)段,它們和共享庫(kù)關(guān)聯(lián)。我相信這些庫(kù)都具有每個(gè)進(jìn)程的地址表。

因?yàn)樗械淖x寫(xiě)段都具有相同的名字,一次要找出出問(wèn)題的部分需要花費(fèi)一點(diǎn)時(shí)間。對(duì)于Java堆,有4個(gè)相關(guān)的大塊內(nèi)存被分配(新生代有2個(gè),老年代1個(gè),***代1個(gè)),他們的大小由GC和堆配置來(lái)決定。

其他問(wèn)題

這部分的內(nèi)容并不是對(duì)所有地方都適用。大部分都是我解決問(wèn)題的過(guò)程中總結(jié)的實(shí)際經(jīng)驗(yàn)。

不要被虛擬內(nèi)存的統(tǒng)計(jì)信息所誤導(dǎo)

有很多抱怨說(shuō)Java是’memory hog’,經(jīng)常被top命令的’VIRT’部分和Windows任務(wù)管理器的’Mem Usage’列所證實(shí)。需要澄清的是,有太多的東西都不會(huì)算進(jìn)這個(gè)統(tǒng)計(jì)信息中,有些還是與其他程序共享的(比如說(shuō)C的庫(kù))。實(shí)際上也有很多‘空’的區(qū)域在虛擬內(nèi)存映射空間中:如果你適用-Xms1000m來(lái)啟動(dòng)JVM,就算你還沒(méi)有開(kāi)始分配對(duì)象,虛擬內(nèi)存的大小也會(huì)超過(guò)1000m。

一個(gè)更好的測(cè)量方法是使用駐留集的大小:你的應(yīng)用程序真正使用的物理內(nèi)存的頁(yè)數(shù),不包含共享頁(yè)。這就是top命令中得’RES’列。但是,駐留集并不是對(duì)你的程序所需使用的總內(nèi)存***的測(cè)量方法。操作系統(tǒng)只有在你的程序真正需要使用它們的時(shí)候才會(huì)將它們放進(jìn)進(jìn)程的內(nèi)存空間中,一般來(lái)說(shuō)是在你的系統(tǒng)處于高負(fù)載的情況下才會(huì)出現(xiàn),這會(huì)花費(fèi)一段較長(zhǎng)的時(shí)間。

***:始終使用工具來(lái)提供所需的詳細(xì)信息來(lái)分析Java中的內(nèi)存問(wèn)題。并且只有當(dāng)出現(xiàn)OOM的時(shí)候才考慮下結(jié)論。

OOM的罪魁禍?zhǔn)捉?jīng)常離它的拋出點(diǎn)很近

內(nèi)存泄露一般在內(nèi)存被分配之后不久發(fā)生。一個(gè)相似的結(jié)論是,OOM的根源一般都離它的拋出點(diǎn)很近,可以使用堆跟蹤技術(shù)來(lái)首先進(jìn)行分析。其基本原理是,內(nèi)存泄露一般和產(chǎn)生大量的內(nèi)存相關(guān)聯(lián)。這說(shuō)明了,導(dǎo)致泄露的代碼具有更高的失敗風(fēng)險(xiǎn)率,不管是因?yàn)槠鋬?nèi)存分配代碼被調(diào)用的過(guò)于頻繁,還是因?yàn)槊看握{(diào)用都分配的過(guò)大的內(nèi)存。因此,可以?xún)?yōu)先考慮使用棧跟蹤來(lái)定位問(wèn)題。

和緩存相關(guān)的部分最值得懷疑

我在這篇文章中提到緩存了很多次:在我數(shù)十年的Java工作經(jīng)歷中發(fā)現(xiàn),和內(nèi)存泄露相關(guān)的類(lèi)進(jìn)場(chǎng)都是和緩存相關(guān)的。實(shí)際上緩存是很難編寫(xiě)的。

使用緩存有很多很多很好的理由,并且使用自己寫(xiě)的緩存也有很多好的理由。如果你確定要使用緩存,請(qǐng)先回答下面的問(wèn)題:

  • 哪些對(duì)象會(huì)被放進(jìn)緩存?如果你所要緩存的對(duì)象都是同一種類(lèi)型(或者具有繼承關(guān)系),那么相比一個(gè)可以容納各種類(lèi)型的緩存來(lái)說(shuō)更好跟蹤問(wèn)題。
  • 有多少對(duì)象會(huì)被同時(shí)放進(jìn)緩存?如果你像讓ProductCache緩存1000個(gè)對(duì)象,但是在內(nèi)存分析結(jié)果中發(fā)現(xiàn)了10000個(gè)對(duì)象,那么這之間的關(guān)系就比較好定位。如果你指定了這個(gè)緩存最多的容量上限,那么你就可以很容易的計(jì)算出這個(gè)緩存最多需要多少內(nèi)存。
  • 過(guò)期和清除策略是什么?每一個(gè)緩存為了控制存在于其中的對(duì)象的存貨周期,都需要一個(gè)明確的驅(qū)逐策略。如果你沒(méi)有指定一個(gè)明確的驅(qū)逐策略,那么有些對(duì)象就很有可能比它真正需要的存活周期要長(zhǎng),占用更多的內(nèi)存,加重垃圾搜集器的負(fù)載(記?。涸跇?biāo)記階段需要的時(shí)間和存活對(duì)象的數(shù)量成正比)。
  • 是否會(huì)在緩存之外同時(shí)持有這些存活對(duì)象的引用?緩存***的應(yīng)用場(chǎng)景是,調(diào)用頻繁,并且調(diào)用時(shí)間很短,并且所緩存的對(duì)象的獲取代價(jià)很大。如果你需要?jiǎng)?chuàng)建一個(gè)對(duì)象,并且在整個(gè)應(yīng)用程序的生命周期中都需要引用這個(gè)對(duì)象,那么就沒(méi)有必要將這個(gè)對(duì)象放入緩存(也許使用池技術(shù)可以顯示總得對(duì)象數(shù)量)。

注意對(duì)象的生命周期

一般來(lái)說(shuō)對(duì)象可以被劃分為兩類(lèi):一類(lèi)是伴隨著整個(gè)程序的生命周期而存活;另外一來(lái)是僅僅存活并服務(wù)于一個(gè)單一的請(qǐng)求。搞清楚這個(gè)非常重要,你僅僅需要關(guān)心你認(rèn)為是長(zhǎng)時(shí)間存活的對(duì)象。

一種方法是在程序啟動(dòng)的時(shí)候全部初始化好所有長(zhǎng)時(shí)間(long-lived)存活的對(duì)象,不管他們是否要立刻被用到。另外一個(gè)方法是使用依賴(lài)注入框架,比如Spring。這不僅僅可以很方便的bean配置文件中找到所有l(wèi)ong-lived的對(duì)象(不需要掃描整個(gè)classpath),還可以很清楚的知道這些對(duì)象在哪里被使用。

查找在方法參數(shù)中被錯(cuò)誤使用的對(duì)象

在大部分場(chǎng)景中,在一個(gè)方法中被分配的對(duì)象都會(huì)在方法退出的時(shí)候被清理掉(除開(kāi)被返回的對(duì)象)。當(dāng)你都是用局部變量來(lái)保存這些對(duì)象的時(shí)候,這個(gè)規(guī)則很容易被遵守。但是,有時(shí)候任然會(huì)使用實(shí)體變量來(lái)保存這些對(duì)象,特別是在方法中會(huì)調(diào)用大量其他方法的時(shí)候,主要是為了避免過(guò)多和麻煩的方法參數(shù)傳遞。

這樣做不是一定會(huì)產(chǎn)生泄漏。后續(xù)的方法調(diào)用會(huì)重新對(duì)這些變量進(jìn)行賦值,這樣就可以讓之前被創(chuàng)建的對(duì)象被回收。但是這樣導(dǎo)致不必要的內(nèi)存開(kāi)銷(xiāo),并且讓調(diào)試更加困難。但是從設(shè)計(jì)的角度出發(fā),當(dāng)我看到這樣的代碼時(shí),我就會(huì)考慮將這個(gè)方法單獨(dú)提出來(lái)形成一個(gè)獨(dú)立的類(lèi)。

J2EE:不要濫用session

session對(duì)象是用來(lái)在多個(gè)請(qǐng)求之間保存和共享用戶(hù)相關(guān)的數(shù)據(jù),主要是因?yàn)镠TTP協(xié)議是無(wú)狀態(tài)的。有時(shí)候它便成了一個(gè)用于緩存的臨時(shí)性解決方案。

這也不是說(shuō)一定就會(huì)產(chǎn)生泄漏,因?yàn)閣eb容器會(huì)在一段時(shí)間后讓用戶(hù)的session失效。但是它卻顯著提高了整個(gè)程序的內(nèi)存占用量,這是很糟糕的。并且它非常難調(diào)試:就像我之前提到的,很難看出對(duì)象被哪些其他的對(duì)象所持有。

小心過(guò)量的垃圾搜集

雖然OOM很糟糕,但是如果不停的執(zhí)行垃圾搜集將會(huì)更加糟糕:它會(huì)搶走本該屬于你的程序的CPU時(shí)間。

有些時(shí)候你僅僅是需要更多的內(nèi)存

就像我在開(kāi)頭的地方所說(shuō)的,JVM是唯一的一個(gè)讓你指定你的數(shù)據(jù)***值(內(nèi)存上限)的現(xiàn)代編程環(huán)境。因此,會(huì)有很多時(shí)候讓你以為發(fā)生了內(nèi)存泄露,但是實(shí)際上你僅僅需要增加你的堆大小。解決內(nèi)存問(wèn)題的***步***還是先增加你的內(nèi)存上限。如果你真的遇到了內(nèi)存泄露問(wèn)題,那么無(wú)論你增加了多少內(nèi)存,你***都還是會(huì)得到OOM的錯(cuò)誤。

責(zé)任編輯:未麗燕 來(lái)源: shenzhang
相關(guān)推薦

2009-03-16 15:47:16

Java線程多線程

2011-12-01 14:56:30

Java字節(jié)碼

2016-05-18 17:15:17

互動(dòng)出版網(wǎng)

2020-10-20 10:17:20

Java泛型Type

2017-02-07 09:54:43

JVMJavaClass

2013-11-26 16:32:47

Android關(guān)機(jī)移動(dòng)編程

2010-09-17 15:44:21

網(wǎng)絡(luò)協(xié)議

2010-10-09 11:20:13

2011-08-03 09:18:39

RIP路由協(xié)議RIP

2023-12-12 07:16:34

HTML標(biāo)簽開(kāi)發(fā)

2013-04-01 10:12:39

2015-05-25 09:45:16

Java多繼承深入解析

2024-11-20 15:55:57

線程Java開(kāi)發(fā)

2025-01-24 08:19:57

2012-03-05 11:09:01

JavaClass

2021-08-30 07:22:14

JVM OutOfMemory異常

2011-07-14 13:09:13

終端服務(wù)入侵檢測(cè)陷阱技術(shù)

2011-06-07 13:58:38

光纖通信光纖

2011-04-07 10:23:00

路由

2011-04-07 10:34:12

路由
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)