輕松搞定Java內(nèi)存泄漏(轉(zhuǎn))[@more@] 抽象
盡管java虛擬機(jī)和垃圾回收機(jī)制管理著大部分的內(nèi)存事務(wù),但是在java軟件中還是可能存在內(nèi)存泄漏的情況。的確,在大型工程中,內(nèi)存泄漏是一個(gè)普遍問(wèn)題。避免內(nèi)存泄漏的第一步,就是要了解他們發(fā)生的原因。這篇文章就是要介紹一些常見(jiàn)的缺陷,然后提供一些非常好的實(shí)踐例子來(lái)指導(dǎo)你寫出沒(méi)有內(nèi)存泄漏的代碼。一旦你的程序存在內(nèi)存泄漏,要查明代碼中引起泄漏的原因是很困難的。同時(shí)這篇文章也要介紹一個(gè)新的工具來(lái)查找內(nèi)存泄漏,然后指明發(fā)生的根本原因。這個(gè)工具容易上手,可以讓你找到產(chǎn)品級(jí)系統(tǒng)中的內(nèi)存泄漏。
垃圾回收(GC)的角色
雖然垃圾回收關(guān)心著大部分的問(wèn)題,包括內(nèi)存管理,使得程序員的任務(wù)顯得更加輕松,但是程序員還是可能犯些錯(cuò)誤導(dǎo)致內(nèi)存泄漏問(wèn)題。GC(垃圾回收)通過(guò)遞歸對(duì)所有從"根"對(duì)象(堆棧中的對(duì)象,靜態(tài)數(shù)據(jù)成員,JNI句柄等等)繼承下來(lái)的引用進(jìn)行工作,然后標(biāo)記所有可以訪問(wèn)的活著的對(duì)象。而這些對(duì)象變成了程序唯一能夠操縱的對(duì)象,其他的對(duì)象都被釋放了。因?yàn)镚C使得程序不能夠訪問(wèn)那些被釋放的對(duì)象,所以這樣做是安全的。
內(nèi)存管理可以說(shuō)是自動(dòng)的,但是這并沒(méi)有讓程序員脫離內(nèi)存管理問(wèn)題。比方說(shuō),對(duì)于內(nèi)存的分配(還有釋放)總是存在一定的開(kāi)銷,盡管這些開(kāi)銷對(duì)程序員來(lái)說(shuō)是暗含的。一個(gè)程序如果創(chuàng)建了很多對(duì)象,那么它就要比完成相同任務(wù)而創(chuàng)建了較少對(duì)象的程序執(zhí)行的速度慢(其他提供的內(nèi)容都相同)。
導(dǎo)致內(nèi)存泄漏主要的原因是,先前申請(qǐng)了內(nèi)存空間而忘記了釋放。如果程序中存在對(duì)無(wú)用對(duì)象的引用,那么這些對(duì)象就會(huì)駐留內(nèi)存,消耗內(nèi)存,因?yàn)闊o(wú)法讓垃圾回收器驗(yàn)證這些對(duì)象是否不再需要。正如我們前面看到的,如果存在對(duì)象的引用,這個(gè)對(duì)象就被定義為"活著的",同時(shí)不會(huì)被釋放。要確定對(duì)象所占內(nèi)存將被回收,程序員就要?jiǎng)?wù)必確認(rèn)該對(duì)象不再會(huì)被使用。典型的做法就是把對(duì)象數(shù)據(jù)成員設(shè)為null或者從集合中移除該對(duì)象。注意,當(dāng)局部變量不需要時(shí),不需明顯的設(shè)為 null,因?yàn)橐粋€(gè)方法執(zhí)行完畢時(shí),這些引用會(huì)自動(dòng)被清理。
從更高一個(gè)層次看,這就是所有存在內(nèi)存管的語(yǔ)言對(duì)內(nèi)存泄漏所考慮的事情,剩余的對(duì)象引用將不再會(huì)被使用。
典型泄漏
既然我們知道了在java中確實(shí)會(huì)存在內(nèi)存泄漏,那么就讓我們看一些典型的泄漏,并找出他們發(fā)生的原因。
全局集合
在大型應(yīng)用程序中存在各種各樣的全局?jǐn)?shù)據(jù)倉(cāng)庫(kù)是很普遍的,比如一個(gè)JNDI-tree或者一個(gè)session table。在這些情況下,注意力就被放在了管理數(shù)據(jù)倉(cāng)庫(kù)的大小上。當(dāng)然是有一些適當(dāng)?shù)臋C(jī)制可以將倉(cāng)庫(kù)中的無(wú)用數(shù)據(jù)移除。
可以有很多不同的解決形式,其中最常用的是一種周期運(yùn)行的清除作業(yè)。這個(gè)作業(yè)會(huì)驗(yàn)證倉(cāng)庫(kù)中的數(shù)據(jù)然后清除一切不需要的數(shù)據(jù)。
另一個(gè)辦法是使用引用計(jì)算。集合用來(lái)對(duì)了解每個(gè)集合入口關(guān)聯(lián)器(referrer)的數(shù)目負(fù)責(zé)。這要求關(guān)聯(lián)器通知集合什么時(shí)候完成進(jìn)入。當(dāng)關(guān)聯(lián)器的數(shù)目為零時(shí),就可以移除集合中的相關(guān)元素。
高速緩存
高速緩存是一種用來(lái)快速查找已經(jīng)執(zhí)行過(guò)的操作結(jié)果的數(shù)據(jù)結(jié)構(gòu)。因此,如果一個(gè)操作執(zhí)行很慢的話,你可以先把普通輸入的數(shù)據(jù)放入高速緩存,然后過(guò)些時(shí)間再調(diào)用高速緩存中的數(shù)據(jù)。
高速緩存多少還有一點(diǎn)動(dòng)態(tài)實(shí)現(xiàn)的意思,當(dāng)數(shù)據(jù)操作完畢,又被送入高速緩存。一個(gè)典型的算法如下所示:
1.檢查結(jié)果是否在高速緩存中,存在則返回結(jié)果;
2.如果結(jié)果不在,那么計(jì)算結(jié)果;
3.將結(jié)果放入高速緩存,以備將來(lái)的操作調(diào)用。
這個(gè)算法的問(wèn)題(或者說(shuō)潛在的內(nèi)存泄漏)在最后一步。如果操作伴隨著一個(gè)不同的,輸入非常大的數(shù)字,那么存入高速緩存的也是一個(gè)非常大的結(jié)果。那么這個(gè)方法就不是能夠勝任的了。
為了避免這種潛在的致命錯(cuò)誤設(shè)計(jì),程序就必須確定高速緩存在他所使用的內(nèi)存中有一個(gè)上界。因此,更好的算法是:
1.檢查結(jié)果是否在高速緩存中,存在則返回結(jié)果;
2.如果結(jié)果不在,那么計(jì)算結(jié)果;
3.如果高速緩存所占空間過(guò)大,移除緩存中舊的結(jié)果;
4.將結(jié)果放入高速緩存,以備將來(lái)的操作調(diào)用。
通過(guò)不斷的從緩存中移除舊的結(jié)果,我們可以假設(shè),將來(lái),最新輸入的數(shù)據(jù)可能被重用的幾率要遠(yuǎn)遠(yuǎn)大于舊的結(jié)果。這通常是一個(gè)不錯(cuò)的設(shè)想。
這個(gè)新的算法會(huì)確保高速緩存的容量在預(yù)先確定的范圍內(nèi)。精確的范圍是很難計(jì)算的,因?yàn)榫彺嬷械膶?duì)象存在引用時(shí)將繼續(xù)有效。正確的劃分高速緩存的大小是一個(gè)復(fù)雜的任務(wù),你必須權(quán)衡可使用內(nèi)存大小和數(shù)據(jù)快速存取之間的矛盾。
另一個(gè)解決這個(gè)問(wèn)題的途徑是使用java.lang.ref.SoftReference類堅(jiān)持將對(duì)象放入高速緩存。這個(gè)方法可以保證當(dāng)虛擬機(jī)用完內(nèi)存或者需要更多堆的時(shí)候,可以釋放這些對(duì)象的引用。
類裝載器
Java類裝載器創(chuàng)建就存在很多導(dǎo)致內(nèi)存泄漏的漏洞。由于類裝載器的復(fù)雜結(jié)構(gòu),使得很難得到內(nèi)存泄漏的透視圖。這些困難不僅僅是由于類裝載器只與"普通的"對(duì)象引用有關(guān),同時(shí)也和對(duì)象內(nèi)部的引用有關(guān),比如數(shù)據(jù)變量,方法和各種類。這意味著只要存在對(duì)數(shù)據(jù)變量,方法,各種類和對(duì)象的類裝載器,那么類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關(guān)聯(lián),同時(shí)也可以和靜態(tài)數(shù)據(jù)變量關(guān)聯(lián),那么相當(dāng)多的內(nèi)存就可能發(fā)生泄漏。
定位內(nèi)存泄漏
常常地,程序內(nèi)存泄漏的最初跡象發(fā)生在出錯(cuò)之后,得到一個(gè)OutOfMemoryError在你的程序中。這種典型地情況發(fā)生在產(chǎn)品環(huán)境中,而在那里,你希望內(nèi)存泄漏盡可能的少,調(diào)試的可能性也達(dá)到最小。也許你的測(cè)試環(huán)境和產(chǎn)品的系統(tǒng)環(huán)境不盡相同,導(dǎo)致泄露的只會(huì)在產(chǎn)品中揭示。這種情況下,你需要一個(gè)低內(nèi)務(wù)操作工具來(lái)監(jiān)聽(tīng)和尋找內(nèi)存泄漏。同時(shí),你還需要把這個(gè)工具同你的系統(tǒng)聯(lián)系起來(lái),而不需要重新啟動(dòng)他或者機(jī)械化你的代碼。也許更重要的是,當(dāng)你做分析的時(shí)候,你需要能夠同工具分離而使得系統(tǒng)不會(huì)受到干擾。
一個(gè)OutOfMemoryError常常是內(nèi)存泄漏的一個(gè)標(biāo)志,有可能應(yīng)用程序的確用了太多的內(nèi)存;這個(gè)時(shí)候,你既不能增加JVM的堆的數(shù)量,也不能改變你的程序而使得他減少內(nèi)存使用。但是,在大多數(shù)情況下,一個(gè) OutOfMemoryError是內(nèi)存泄漏的標(biāo)志。一個(gè)解決辦法就是繼續(xù)監(jiān)聽(tīng)GC的活動(dòng),看看隨時(shí)間的流逝,內(nèi)存使用量是否會(huì)增加,如果有,程序中一定存在內(nèi)存泄漏。
詳細(xì)輸出
有很多辦法來(lái)監(jiān)聽(tīng)垃圾回收器的活動(dòng)。也許運(yùn)用最廣泛的就是以:-Xverbose:gc選項(xiàng)運(yùn)行JVM,然后觀察輸出結(jié)果一段時(shí)間。
[memory] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms
箭頭后的值(在這個(gè)例子中 16788K)是垃圾回收后堆的使用量。
控制臺(tái)
觀察這些無(wú)盡的GC詳細(xì)統(tǒng)計(jì)輸出是一件非常單調(diào)乏味的事情。好在有一些工具來(lái)代替我們做這些事情。The JRockit Management Console可以用圖形的方式輸出堆的使用量。通過(guò)觀察圖像,我們可以很方便的觀察堆的使用量是否伴隨時(shí)間增長(zhǎng)。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0>
Figure 1. The JRockit Management Console
管理控制臺(tái)甚至可以配置成在堆使用量出現(xiàn)問(wèn)題(或者其他的事件發(fā)生)時(shí)向你發(fā)送郵件。這個(gè)顯然使得監(jiān)控內(nèi)存泄漏更加容易。
內(nèi)存泄漏探測(cè)工具
有很多專門的內(nèi)存泄漏探測(cè)工具。其中The JRockit Memory Leak Detector可以供來(lái)觀察內(nèi)存泄漏也可以針對(duì)性地找到泄漏的原因。這個(gè)強(qiáng)大的工具被緊密地集成在JRockit JVM中,可以提供最低可能的內(nèi)存事務(wù)也可以輕松的訪問(wèn)虛擬機(jī)的堆。
專門工具的優(yōu)勢(shì)
一旦你知道程序中存在內(nèi)存泄漏,你需要更專業(yè)的工具來(lái)查明為什么這里會(huì)有泄漏。而JVM是不可能告訴你的?,F(xiàn)在有很多工具可以利用了。這些工具本質(zhì)上主要通過(guò)兩種方法來(lái)得到JVM的存儲(chǔ)系統(tǒng)信息的:JVMTI和字節(jié)碼使用儀器。Java虛擬機(jī)工具接口(JVMTI)和他的原有形式JVMPI(壓型接口)都是標(biāo)準(zhǔn)接口,作為外部工具同JVM進(jìn)行通信,搜集JVM的信息。字節(jié)碼使用儀器則是引用通過(guò)探針獲得工具所需的字節(jié)信息的預(yù)處理技術(shù)。
通過(guò)這些技術(shù)來(lái)偵測(cè)內(nèi)存泄漏存在兩個(gè)缺點(diǎn),而這使得他們?cè)诋a(chǎn)品級(jí)環(huán)境中的運(yùn)用不夠理想。首先,根據(jù)兩者對(duì)內(nèi)存的使用量和內(nèi)存事務(wù)性能的降級(jí)是不可以忽略的。從JVM 獲得的堆的使用量信息需要在工具中導(dǎo)出,收集和處理。這意味著要分配內(nèi)存。按照J(rèn)VM的性能導(dǎo)出信息是需要開(kāi)銷的,垃圾回收器在搜集信息的時(shí)候是運(yùn)行的非常緩慢的。另一個(gè)缺點(diǎn)就是,這些工具所需要的信息是關(guān)系到JVM的。讓工具在JVM開(kāi)始運(yùn)行的時(shí)候和它關(guān)聯(lián),而在分析的時(shí)候,分離工具而保持JVM運(yùn)行,這顯然是不可能的。
既然JRockit Memory Leak Detector是被集成到JVM中的,那么以上兩種缺點(diǎn)就不再適用。首先,大部分的處理和分析都是在JVM中完成的,所以就不再需要傳送或重建任何數(shù)據(jù)。處理也可以在垃圾回收器的背上,他的意思是提高速度。再有,內(nèi)存泄漏偵測(cè)器可以同一個(gè)運(yùn)行的JVM關(guān)聯(lián)和分離,只要JVM在開(kāi)始的時(shí)候伴隨著 -Xmanagement選項(xiàng)(這個(gè)允許監(jiān)聽(tīng)和管理JVM通過(guò)遠(yuǎn)程JMX接口)。當(dāng)工具分離以后,工具不會(huì)遺留任何東西在JVM中;JVM就可以全速運(yùn)行代碼就好像工具關(guān)聯(lián)之前一樣。
趨勢(shì)分析
讓我們更深一步來(lái)觀察這個(gè)工具,了解他如何捕捉到內(nèi)存泄漏。在你了解到代碼中存在內(nèi)存泄漏,第一步就是嘗試計(jì)算出什么數(shù)據(jù)在泄漏--哪個(gè)對(duì)象類導(dǎo)致泄露。The JRockit Memory Leak Detector通過(guò)在垃圾回收的時(shí)候,計(jì)算每個(gè)類所包含的現(xiàn)有的對(duì)象來(lái)達(dá)到目的。如果某一個(gè)類的對(duì)象成員數(shù)目隨著時(shí)間增長(zhǎng)(增長(zhǎng)率),那么這里很可能存在泄漏。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0>
igure 2. The trend analysis view of the Memory Leak Detector
因?yàn)橐粋€(gè)泄漏很可能只是像水滴一樣小,所以趨勢(shì)分析必須運(yùn)行足夠長(zhǎng)的一段時(shí)間。在每個(gè)短暫的時(shí)間段里,局部類的增加會(huì)使得泄漏發(fā)生推遲。但是,內(nèi)存事務(wù)是非常小的(最大的內(nèi)存事務(wù)是由在每個(gè)垃圾回收時(shí)從JRockit向內(nèi)存泄漏探測(cè)器發(fā)送的一個(gè)數(shù)據(jù)包組成的)。內(nèi)存事務(wù)不應(yīng)該成為任何系統(tǒng)的問(wèn)題--甚至一個(gè)在產(chǎn)品階段全速運(yùn)行的程序。
一開(kāi)始,數(shù)字會(huì)有很大的跳轉(zhuǎn),隨時(shí)間的推進(jìn),這些數(shù)字會(huì)變得穩(wěn)定,而后顯示哪些類會(huì)不斷的增大。
尋找根本原因
知道那些對(duì)象的類會(huì)導(dǎo)致泄露,有時(shí)候足夠制止泄露問(wèn)題。這個(gè)類也許只是被用在非常有限的部分,通過(guò)快速的視察就可以找到問(wèn)題所在。不幸的是,這些信息是不夠的。比方說(shuō),經(jīng)常導(dǎo)致內(nèi)存泄漏的對(duì)象類java.lang.String,然而String類被應(yīng)用于整個(gè)程序,這就變得有些無(wú)助。
我們想知道的是其他的對(duì)象是否會(huì)導(dǎo)致內(nèi)存泄漏,好比上面提到的String類,為什么這些導(dǎo)致泄漏的對(duì)象還是存在周圍?那些引用是指向這些對(duì)象的?這里一列的對(duì)象存有對(duì)String類的引用,就會(huì)變得太大而沒(méi)有實(shí)際意義。為了限制數(shù)據(jù)的數(shù)量,我們可以通過(guò)類把他們編成一個(gè)組,這樣我們就可以看到,那些其他類的對(duì)象會(huì)依然泄漏對(duì)象(String類)。比如,將一個(gè)String類放入Hashtable,那里我們可以看到關(guān)聯(lián)到String類的 Hashtable入口。從Hashtable入口向后運(yùn)行,我們終于找到那些關(guān)聯(lián)到String類的Hashtable對(duì)象(參看圖三如下)。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0 resized="true">
Figure 3. Sample view of the type graph as seen in the tool
向后工作
自從開(kāi)始我們就一直著眼于對(duì)象類,而不是單獨(dú)的對(duì)象,我們不知道那個(gè)Hashtable存在泄漏。如果我們可以找出所有的Hashtable在系統(tǒng)中有多大,我們可以假設(shè)最大的那個(gè)Hashtable存在泄漏(因?yàn)樗梢跃奂銐虻男孤┒兊煤艽螅?。因此,所有Hashtable,同時(shí)有和所有他們所涉及的數(shù)據(jù),可以幫助我們查明導(dǎo)致泄露的精確的Hashtable。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0>
Figure 4. Screenshot of the list of Hashtable objects and the size of the data they are holding live
計(jì)算一個(gè)對(duì)象所涉及的數(shù)據(jù)的開(kāi)銷是非常大的(這要求引用圖表伴隨著那個(gè)對(duì)象作為根運(yùn)行)而且如果對(duì)每一個(gè)對(duì)象都這樣處理,就需要很多時(shí)間。知道一些關(guān)于 Hashtable內(nèi)部的實(shí)現(xiàn)機(jī)制可以帶來(lái)捷徑。在內(nèi)部,一個(gè)Hashtable有一個(gè)Hashtable的數(shù)組入口。數(shù)組的增長(zhǎng)伴隨著 Hashtable中對(duì)象的增長(zhǎng)。因此,要找到最大的Hashtable,我們可以把搜索限制在尋找包含Hashtable引用入口的最大的數(shù)組。這樣就更快捷了。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0>
Figure 5. Screenshot of the listing of the largest Hashtable entry arrays, as well as their sizes.
向下深入
當(dāng)我們發(fā)現(xiàn)了存在泄漏的Hashtable的實(shí)例,就可以順藤摸瓜找到其他的引用這些Hashtable的實(shí)例,然后用上面的方法來(lái)找到是那個(gè)Hashtable存在問(wèn)題。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0 resized="true">
Figure 6. This is what an instance graph can look like in the tool.
舉個(gè)例子,一個(gè)Hashtable可以有一個(gè)來(lái)自MyServer的對(duì)象的引用,而MyServer包含一個(gè)activeSessions數(shù)據(jù)成員。這些信息就足夠深入代碼找出問(wèn)題所在。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0>
Figure 7. Inspecting an object and its references to other objects
找出分配點(diǎn)
當(dāng)發(fā)現(xiàn)了內(nèi)存泄漏問(wèn)題,找到那些泄漏的對(duì)象在何處是非常有用的。也許沒(méi)有足夠的信息知道他們同其他相關(guān)對(duì)象之間的聯(lián)系,但是關(guān)于他們?cè)谀抢锉粍?chuàng)建的信息還是很有幫助的。當(dāng)然,你不會(huì)愿意創(chuàng)建一個(gè)工具來(lái)打印出所有分配的堆棧路徑。你也不會(huì)愿意在模擬環(huán)境中運(yùn)行程序只是為了捕捉到一個(gè)內(nèi)存泄漏。
有了JRockit Memory Leak Detector,程序代碼可以動(dòng)態(tài)的在內(nèi)存分配出創(chuàng)建堆棧路徑。這些堆棧路徑可以在工具中累積,分析。如果你不啟用這個(gè)工具,這個(gè)特征就不會(huì)有任何消耗,這就意味著時(shí)刻準(zhǔn)備著開(kāi)始。當(dāng)需要分配路徑時(shí),JRockit的編譯器可以讓代碼不工作,而監(jiān)視內(nèi)存分配,但只對(duì)需要的特定類有效。更好的是,當(dāng)做完數(shù)據(jù)分析后,生成的機(jī)械代碼會(huì)完全被移除,不會(huì)引起任何執(zhí)行上的效率衰退。
400) {this.resized=true; this.width=400; this.alt='Click here to open new window';}" border=0>
Figure 8. The allocation stack traces for String during execution of a sample program
總結(jié)
內(nèi)存泄漏查找起來(lái)非常困難,文章中的一些避免泄漏的好的實(shí)踐,包括了要時(shí)刻記住把什么放進(jìn)了數(shù)據(jù)結(jié)構(gòu)中,更接近的監(jiān)視內(nèi)存中意外的增長(zhǎng)。
我們同時(shí)也看到了JRockit Memory Leak Detector是如何捕捉產(chǎn)品級(jí)系統(tǒng)中的內(nèi)存泄漏的。該工具通過(guò)三步的方法發(fā)現(xiàn)泄漏。一,通過(guò)趨勢(shì)分析發(fā)現(xiàn)那些對(duì)象類存在泄漏;二,找出同泄漏對(duì)象相關(guān)的其他類;三,向下發(fā)掘,觀察獨(dú)立的對(duì)象之間是如何相互聯(lián)系的。同時(shí),該工具也可以動(dòng)態(tài)的,找出所有內(nèi)存分配的堆棧路徑。利用這三個(gè)特性,將該工具緊緊地集成在JVM中,那么就可以安全的,有效的捕捉和修復(fù)內(nèi)存泄漏了。
分享標(biāo)題:輕松搞定Java內(nèi)存泄漏(轉(zhuǎn))
標(biāo)題URL:http://www.rwnh.cn/article26/jjedcg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站導(dǎo)航、全網(wǎng)營(yíng)銷推廣、網(wǎng)站營(yíng)銷、品牌網(wǎng)站建設(shè)、網(wǎng)站排名、定制開(kāi)發(fā)
廣告
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源:
創(chuàng)新互聯(lián)