java 内存泄露
http://www.iteye.com/topic/626801
String.substring?
并沒有生成一個新的String, 而是返回一個指向同一個數組的“String”, 并修改其offset和count, 所以容易造成內存溢出 本來String是一個非常大的,我只需要sub其中很小的一塊,但是這很小的一塊所指向的內存和大塊所指向的內存是一樣的,所以大塊內存還不會釋放如果把sub出來的string放入一個map中,那么這個map就很容易內存溢出了
一直贊嘆Sun對待技術的嚴謹和優雅(可憐的Sun)。Sun JDK中Java庫的源代碼,連注釋都清清楚楚、規規范范,javadoc注解的使用也一絲不茍,讀起來很熟舒服。因此,在日常工作和學習中,經常讀讀 Java庫的源代碼,不亦樂乎?如果遇到詭異問題,源代碼的幫助就更大了。
?
??? 閑話少說,回歸正題。這幾天,一直在為Java的“內存泄露”問題糾結。Java應用程序占用的內存在不斷的、有規律的上漲,最終超過了監控閾值。福爾摩 斯不得不出手了!
?
??? 說起Java的內存泄露,其實定義不是那么明確。首先,如果JVM沒有bug,那么理論上是不會出現“無法回收的堆空間”,也就是說C/C++中的那種內 存泄露在Java中不存在的。其次,如果由于Java程序一直持有某個對象的引用,但是從程序邏輯上看,這個對象再也不會被用到了,那么我們可以認為這個 對象被泄露了。如果這樣的對象數量很多,那么很明顯,大量的內存空間就被泄露(“浪費”更準確一些)了。
?
??? 不過,本文要說的內存泄露,并不屬于上述原因,因此打上了引號。其具體原因,確實出乎意料。欲知詳情,請看下面講解。
分析內存泄露的一般步驟
?
??? 如果發現Java應用程序占用的內存出現了泄露的跡象,那么我們一般采用下面的步驟分析
dump heap
?
??? 如果Java應用程序出現了內存泄露,千萬別著急著把應用殺掉,而是要保存現場。如果是互聯網應用,可以把流量切到其他服務器。保存現場的目的就是為了把 運行中JVM的heap dump下來。
?
??? JDK自帶的jmap工具,可以做這件事情。它的執行方法是:
Java代碼????? format=b的含義是,dump出來的文件時二進制格式。
??? file-heap.bin的含義是,dump出來的文件名是heap.bin。
??? <pid>就是JVM的進程號。
??? (在linux下)先執行ps aux | grep java,找到JVM的pid;然后再執行jmap -dump:format=b,file=heap.bin <pid>,得到heap dump文件。
analyze heap
?
??? 將二進制的heap dump文件解析成human-readable的信息,自然是需要專業工具的幫助,這里推薦Memory Analyzer?。
?
??? Memory Analyzer,簡稱MAT,是Eclipse基金會的開源項目,由SAP和IBM捐助。巨頭公司出品的軟件還是很中用的,MAT可以分析包含數億級對 象的heap、快速計算每個對象占用的內存大小、對象之間的引用關系、自動檢測內存泄露的嫌疑對象,功能強大,而且界面友好易用。
?
??? MAT的界面基于Eclipse開發,以兩種形式發布:Eclipse插件和Eclipe RCP。MAT的分析結果以圖片和報表的形式提供,一目了然。總之個人還是非常喜歡這個工具的。下面先貼兩張官方的screenshots:
??? 言歸正傳,我用MAT打開了heap.bin,很容易看出,char[]的數量出其意料的多,占用90%以上的內存?。一般來說,char[]在JVM確實會占用很多內存,數量也非常多,因為String對象以char[]作為內部存儲。但是這次的char[]太貪婪 了,仔細一觀察,發現有數萬計的char[],每個都占用數百K的內存?。這個現象說明,Java程序保存了數以萬計的大String對象?。結合程序的邏輯,這個是不應該的,肯定在某個地方出了問題。
?
順藤摸瓜
?
????在可疑的char[]中,任意挑了一個,使用Path To GC Root功能,找到該char[]的引用路徑,發現String對象是被一個HashMap中引用的?。這個也是意料中的事情,Java的內存泄露多半是因為對象被遺留在全局的HashMap中得不到釋放。不過,該HashMap被用作一個緩存,設置了緩 存條目的閾值,導達到閾值后會自動淘汰。從這個邏輯分析,應該不會出現內存泄露的。雖然緩存中的String對象已經達到數萬計,但仍然沒有達到預先設置 的閾值(閾值設置地比較大,因為當時預估String對象都比較小)。
?
??? 但是,另一個問題引起了我的注意:為什么緩存的String對象如此巨大?內部char[]的長度達數百K。雖然緩存中的 String對象數量還沒有達到閾值,但是String對象大小遠遠超出了我們的預期,最終導致內存被大量消耗,形成內存泄露的跡象(準確說應該是內存消 耗過多)?。
?
??? 就這個問題進一步順藤摸瓜,看看String大對象是如何被放到HashMap中的。通過查看程序的源代碼,我發現,確實有String大對象,不 過并沒有把String大對象放到HashMap中,而是把String大對象進行split(調用String.split方法),然后將split出 來的String小對象放到HashMap中?了。
?
??? 這就奇怪了,放到HashMap中明明是split之后的String小對象,怎么會占用那么大空間呢?難道是String類的split方法有問題?
?
查看代碼
?
??? 帶著上述疑問,我查閱了Sun JDK6中String類的代碼,主要是是split方法的實現:
Java代碼?
可以看出,Stirng.split方法調用了Pattern.split方法。繼續看Pattern.split方法的代碼:
Java代碼?
??? 注意看第9行:Stirng match = input.subSequence(intdex, m.start()).toString();
這里的match就是split出來的String小對象,它其實是String大對象subSequence的結果。繼續看 String.subSequence的代碼:
Java代碼?
??? String.subSequence有調用了String.subString,繼續看:
Java代碼?
??? 看第11、12行,我們終于看出眉目,如果subString的內容就是完整的原字符串,那么返回原String對象;否則,就會創建一個新的 String對象,但是這個String對象貌似使用了原String對象的char[]。我們通過String的構造函數確認這一點:
Java代碼?
??? 為了避免內存拷貝、加快速度,Sun JDK直接復用了原String對象的char[],偏移量和長度來標識不同的字符串內容。也就是說,subString出的來String小對象 仍然會指向原String大對象的char[],split也是同樣的情況?。這就解釋了,為什么HashMap中String對象的char[]都那么大。
原因解釋
?
??? 其實上一節已經分析出了原因,這一節再整理一下:
解決方案
?
??? 原因找到了,解決方案也就有了。split是要用的,但是我們不要把split出來的String對象直接放到HashMap中,而是調用一下 String的拷貝構造函數String(String original),這個構造函數是安全的,具體可以看代碼:
Java代碼?
??? 只是,new String(string)的代碼很怪異,囧。或許,subString和split應該提供一個選項,讓程序員控制是否復用String對象的 char[]。
是否Bug
?
??? 雖然,subString和split的實現造成了現在的問題,但是這能否算String類的bug呢?個人覺得不好說。因為這樣的優化是比較合理 的,subString和spit的結果肯定是原字符串的連續子序列。只能說,String不僅僅是一個核心類,它對于JVM來說是與原始類型同等重要的 類型。
?
??? JDK實現對String做各種可能的優化都是可以理解的。但是優化帶來了憂患,我們程序員足夠了解他們,才能用好他們。
一些補充
有個地方我沒有說清楚。
?
我的程序是一個Web程序,每次接受請求,就會創建一個大的String對象,然后對該String對象進行split,最后split之后的String對象放到全局緩存中。如果接收了5W個請求,那么就會有5W個大String對象。這5W個大String對象都被存儲在全局緩存中,因此會造成內存泄漏。我原以為緩存的是5W個小String,結果都是大String。
?
“拋出異常的愛”同學,在回帖(第7頁)中建議用"java.io.StreamTokenizer"來解決本文的問題。確實是終極解決方案,比我上面提到的“new String()”,要好很多很多。
總結
- 上一篇: jdk小工具
- 下一篇: Integer.valueOf(Stri