分析和解決JAVA 記憶體泄露的實戰例子__Java

來源:互聯網
上載者:User

這幾天,一直在為Java的“記憶體泄露”問題糾結。Java應用程式佔用的記憶體在不斷的、有規律的上漲,最終超過了監控閾值。福爾摩 斯不得不出手了。

記憶體溢出 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢出。

記憶體泄露 memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體泄露危害可以忽略,但記憶體泄露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。

memory leak會最終會導致out of memory。

記憶體溢出就是你要求分配的記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢出。 

    記憶體流失是指你向系統申請分配記憶體進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊記憶體你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程式。一個盤子用盡各種方法只能裝4個果子,你裝了5個,結果掉倒地上不能吃了。這就是溢出。比方說棧,棧滿時再做進棧必定產生空間溢出,叫上溢,棧空時再做退棧也產生空間溢出,稱為下溢。就是分配的記憶體不足以放下資料項目序列,稱為記憶體溢出.      從使用者使用程式的角度來看,記憶體流失本身不會產生什麼危害,作為一般的使用者,根本感覺不到記憶體流失的存在。真正有危害的是記憶體流失的堆積,這會最終消耗盡系統所有的記憶體。從這個角度來說,一次性記憶體流失並沒有什麼危害,因為它不會堆積,而隱式記憶體流失危害性則非常大,因為較之於常發性和偶發性記憶體流失它更難被檢測到
分析記憶體泄露的一般步驟

     如果發現Java應用程式佔用的記憶體出現了泄露的跡象,那麼我們一般採用下面的步驟分析

00001. 把Java應用程式使用的heap dump下來

00002. 使用Java heap分析工具,找出記憶體佔用超出預期(一般是因為數量太多)的嫌疑對象

00003. 必要時,需要分析嫌疑對象和其他對象的參考關聯性。

00004. 查看程式的原始碼,找出嫌疑對象數量過多的原因。 dump heap

    如果Java應用程式出現了記憶體泄露,千萬別著急著把應用殺掉,而是要儲存現場。如果是互連網應用,可以把流量切到其他伺服器。儲存現場的目的就是為了把 運行中JVM的heap dump下來。

    JDK內建的jmap工具,可以做這件事情。它的執行方法是:

Java代碼   

00001. jmap -dump:format=b,file=heap.bin <pid>  

 

    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代碼 

00001. public   

00002. String[] split(String regex, int limit) {  

00003.     return Pattern.compile(regex).split(this, limit);  

00004. }  

可以看出,Stirng.split方法調用了Pattern.split方法。繼續看Pattern.split方法的代碼:

Java代碼 

00001. public   

00002. String[] split(CharSequence input, int limit) {  

00003.         int index = 0;  

00004.         boolean matchLimited = limit > 0;  

00005.         ArrayList<String> matchList = new   

00006. ArrayList<String>();  

00007.         Matcher m = matcher(input);  

00008.         // Add segments before each match found  

00009.         while(m.find()) {  

00010.             if (!matchLimited || matchList.size() < limit - 1) {  

00011.                 String match = input.subSequence(index,   

00012. m.start()).toString();  

00013.                 matchList.add(match);  

00014.                 index = m.end();  

00015.             } else if (matchList.size() == limit - 1) { // last one  

00016.                 String match = input.subSequence(index,  

00017.                                                    

00018. input.length()).toString();  

00019.                 matchList.add(match);  

00020.                 index = m.end();  

00021.             }  

00022.         }  

00023.         // If no match was found, return this  

00024.         if (index == 0)  

00025.             return new String[] {input.toString()};  

00026.         // Add remaining segment  

00027.         if (!matchLimited || matchList.size() < limit)  

00028.             matchList.add(input.subSequence(index,   

00029. input.length()).toString());  

00030.         // Construct result  

00031.         int resultSize = matchList.size();  

00032.         if (limit == 0)  

00033.             while (resultSize > 0 &&   

00034. matchList.get(resultSize-1).equals(""))  

00035.                 resultSize--;  

00036.         String[] result = new String[resultSize];  

00037.         return matchList.subList(0, resultSize).toArray(result);  

00038.     }  

    注意看第9行:Stirng match = input.subSequence(intdex, m.start()).toString();

這裡的match就是split出來的String小對象,它其實是String大對象subSequence的結果。繼續看 String.subSequence的代碼:

Java代碼 

00001. public   

00002. CharSequence subSequence(int beginIndex, int endIndex) {  

00003.         return this.substring(beginIndex, endIndex);  

00004. }  

    String.subSequence有調用了String.subString,繼續看:

Java代碼 

00001. public String   

00002. substring(int beginIndex, int endIndex) {  

00003.     if (beginIndex < 0) {  

00004.         throw new StringIndexOutOfBoundsException(beginIndex);  

00005.     }  

00006.     if (endIndex > count) {  

00007.         throw new StringIndexOutOfBoundsException(endIndex);  

00008.     }  

00009.     if (beginIndex > endIndex) {  

00010.         throw new StringIndexOutOfBoundsException(endIndex - beginIndex);  

00011.     }  

00012.     return ((beginIndex == 0) && (endIndex == count)) ? this :  

00013.         new String(offset + beginIndex, endIndex - beginIndex, value);  

00014.     }  

    看第11、12行,我們終於看出眉目,如果subString的內容就是完整的原字串,那麼返回原String對象;否則,就會建立一個新的 String對象,但是這個String對象貌似使用了原String對象的char[]。我們通過String的建構函式確認這一點:

Java代碼 

00001. // Package   

00002. private constructor which shares value array for speed.  

00003.     String(int offset, int count, char value[]) {  

00004.     this.value = value;  

00005.     this.offset = offset;  

00006.     this.count = count;  

00007.     }  

    為了避免記憶體拷貝、加快速度,Sun JDK直接複用了原String對象的char[],位移量和長度來標識不同的字串內容。也就是說,subString出的來String小對象 仍然會指向原String大對象的char[],split也是同樣的情況 。這就解釋了,為什麼HashMap中String對象的char[]都那麼大。 原因解釋

 

    其實上一節已經分析出了原因,這一節再整理一下:

00001. 程式從每個請求中得到一個String大對象,該對象內部char[]的長度達數百K。

00002. 程式對String大對象做split,將split得到的String小對象放到HashMap中,用作緩衝。

00003. Sun JDK6對String.split方法做了最佳化,split出來的Stirng對象直接使用原String對象的char[]

00004. HashMap中的每個String對象其實都指向了一個巨大的char[]

00005. HashMap的上限是萬級的,因此被緩衝的Sting對象的總大小=萬*百K=G級。

00006. G級的記憶體被緩衝佔用了,大量的記憶體被浪費,造成記憶體泄露的跡象。 解決方案

 

    原因找到了,解決方案也就有了。split是要用的,但是我們不要把split出來的String對象直接放到HashMap中,而是調用一下 String的拷貝建構函式String(String original),這個建構函式是安全的,具體可以看代碼:

Java代碼 

00001.     /** 

00002.      * Initializes a newly created {@code String} object so that it  

00003. represents 

00004.      * the same sequence of characters as the argument; in other words,  

00005. the 

00006.      * newly created string is a copy of the argument string. Unless an 

00007.      * explicit copy of {@code original} is needed, use of this  

00008. constructor is 

00009.      * unnecessary since Strings are immutable. 

00010.      * 

00011.      * @param  original 

00012.      *         A {@code String} 

00013.      */  

00014.     public String(String original) {  

00015.     int size = original.count;  

00016.     char[] originalValue = original.value;  

00017.     char[] v;  

00018.     if (originalValue.length > size) {  

00019.         // The array representing the String is bigger than the new  

00020.         // String itself.  Perhaps this constructor is being called  

00021.         // in order to trim the baggage, so make a copy of the array.  

00022.             int off = original.offset;  

00023.             v = Arrays.copyOfRange(originalValue, off, off+size);  

00024. <

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.