1. 程式人生 > >分析和解決JAVA 記憶體洩露的實戰例子

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

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

分析記憶體洩露的一般步驟

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

  1. 把Java應用程式使用的heap dump下來
  2. 使用Java heap分析工具,找出記憶體佔用超出預期(一般是因為數量太多)的嫌疑物件
  3. 必要時,需要分析嫌疑物件和其他物件的引用關係。
  4. 檢視程式的原始碼,找出嫌疑物件數量過多的原因。

dump heap

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

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

Java程式碼   收藏程式碼
  1. 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的分析結果概述

MAT分析物件的大小及數量

    言歸正傳,我用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程式碼 
  1. public   
  2. String[] split(String regex, int limit) {  
  3.     return Pattern.compile(regex).split(this, limit);  
  4. }  

可以看出,Stirng.split方法呼叫了Pattern.split方法。繼續看Pattern.split方法的程式碼:

Java程式碼 
  1. public   
  2. String[] split(CharSequence input, int limit) {  
  3.         int index = 0;  
  4.         boolean matchLimited = limit > 0;  
  5.         ArrayList<String> matchList = new   
  6. ArrayList<String>();  
  7.         Matcher m = matcher(input);  
  8.         // Add segments before each match found  
  9.         while(m.find()) {  
  10.             if (!matchLimited || matchList.size() < limit - 1) {  
  11.                 String match = input.subSequence(index,   
  12. m.start()).toString();  
  13.                 matchList.add(match);  
  14.                 index = m.end();  
  15.             } else if (matchList.size() == limit - 1) { // last one  
  16.                 String match = input.subSequence(index,  
  17. input.length()).toString();  
  18.                 matchList.add(match);  
  19.                 index = m.end();  
  20.             }  
  21.         }  
  22.         // If no match was found, return this  
  23.         if (index == 0)  
  24.             return new String[] {input.toString()};  
  25.         // Add remaining segment  
  26.         if (!matchLimited || matchList.size() < limit)  
  27.             matchList.add(input.subSequence(index,   
  28. input.length()).toString());  
  29.         // Construct result  
  30.         int resultSize = matchList.size();  
  31.         if (limit == 0)  
  32.             while (resultSize > 0 &&   
  33. matchList.get(resultSize-1).equals(""))  
  34.                 resultSize--;  
  35.         String[] result = new String[resultSize];  
  36.         return matchList.subList(0, resultSize).toArray(result);  
  37.     }  

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

這裡的match就是split出來的String小物件,它其實是String大物件subSequence的結果。繼續看 String.subSequence的程式碼:

Java程式碼 
  1. public   
  2. CharSequence subSequence(int beginIndex, int endIndex) {  
  3.         return this.substring(beginIndex, endIndex);  
  4. }  

    String.subSequence有呼叫了String.subString,繼續看:

Java程式碼 
  1. public String   
  2. substring(int beginIndex, int endIndex) {  
  3.     if (beginIndex < 0) {  
  4.         throw new StringIndexOutOfBoundsException(beginIndex);  
  5.     }  
  6.     if (endIndex > count) {  
  7.         throw new StringIndexOutOfBoundsException(endIndex);  
  8.     }  
  9.     if (beginIndex > endIndex) {  
  10.         throw new StringIndexOutOfBoundsException(endIndex - beginIndex);  
  11.     }  
  12.     return ((beginIndex == 0) && (endIndex == count)) ? this :  
  13.         new String(offset + beginIndex, endIndex - beginIndex, value);  
  14.     }  

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

Java程式碼 
  1. // Package   
  2. private constructor which shares value array for speed.  
  3.     String(int offset, int count, char value[]) {  
  4.     this.value = value;  
  5.     this.offset = offset;  
  6.     this.count = count;  
  7.     }  

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

原因解釋

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

  1. 程式從每個請求中得到一個String大物件,該物件內部char[]的長度達數百K。
  2. 程式對String大物件做split,將split得到的String小物件放到HashMap中,用作快取。
  3. Sun JDK6對String.split方法做了優化,split出來的Stirng物件直接使用原String物件的char[]
  4. HashMap中的每個String物件其實都指向了一個巨大的char[]
  5. HashMap的上限是萬級的,因此被快取的Sting物件的總大小=萬*百K=G級。
  6. G級的記憶體被快取佔用了,大量的記憶體被浪費,造成記憶體洩露的跡象。

解決方案

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

Java程式碼 
  1.     /** 
  2.      * Initializes a newly created {@code String} object so that it  
  3. represents 
  4.      * the same sequence of characters as the argument; in other words,  
  5. the 
  6.      * newly created string is a copy of the argument string. Unless an 
  7.      * explicit copy of {@code original} is needed, use of this  
  8. constructor is 
  9.      * unnecessary since Strings are immutable. 
  10.      * 
  11.      * @param  original 
  12.      *         A {@code String} 
  13.      */  
  14.     public String(String original) {  
  15.     int size = original.count;  
  16.     char[] originalValue = original.value;  
  17.     char[] v;  
  18.     if (originalValue.length > size) {  
  19.         // The array representing the String is bigger than the new  
  20.         // String itself.  Perhaps this constructor is being called  
  21.         // in order to trim the baggage, so make a copy of the array.  
  22.             int off = original.offset;  
  23.             v = Arrays.copyOfRange(originalValue, off, off+size);  
  24.     } else {  
  25.         // The array representing the String is the same  
  26.         // size as the String, so no point in making a copy.  
  27.         v = originalValue;  
  28.     }  
  29.     this.offset = 0;  
  30.     this.count = size;  
  31.     this.value = v;  
  32.     }  

    只是,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()”,要好很多很多。

相關推薦

分析解決JAVA 記憶體洩露實戰例子

這幾天,一直在為Java的“記憶體洩露”問題糾結。Java應用程式佔用的記憶體在不斷的、有規律的上漲,最終超過了監控閾值。福爾摩 斯不得不出手了! 分析記憶體洩露的一般步驟     如果發現Java應用程式佔用的記憶體出現了洩露的跡象,那麼我們一般採用下面的步驟分

JAVA記憶體洩露分析解決方案及WINDOWS自帶檢視工具

Java記憶體洩漏是每個Java程式設計師都會遇到的問題,程式在本地執行一切正常,可是佈署到遠端就會出現記憶體無限制的增長,最後系統癱瘓,那麼如何最快最好的檢測程式的穩定性,防止系統崩盤,作者用自已的親身經歷與各位分享解決這些問題的辦法.作為Internet最流行的程式語言之一,Java現正非常流行.我們的網

Android中使用Handler造成記憶體洩露分析解決

Java使用有向圖機制,通過GC自動檢查記憶體中的物件(什麼時候檢查由虛擬機器決定),如果GC發現一個或一組物件為不可到達狀態,則將該物件從記憶體中回收。也就是說,一個物件不被任何引用所指向,則該物件會在被GC發現的時候被回收;另外,如果一組物件中只包含互相的引用,而沒有來自它們外部的引用(例如有兩個物件A和

Handler記憶體洩露分析解決辦法以及實現延時執行操作的幾種方法

一.Handler記憶體洩露的分析和解決辦法在進行非同步操作時,我們經常會使用到Handler類。最常見的寫法如下。public class MainActivity extends Activity

JAVA 記憶體洩露詳解(原因、例子解決

  Java的一個重要特性就是通過垃圾收集器(GC)自動管理記憶體的回收,而不需要程式設計師自己來釋放記憶體。理論上Java中所有不會再被利用的物件所佔用的記憶體,都可以被GC回收,但是Java也存在記憶體洩露,但它的表現與C++不同。 JAVA 中的記憶體管理

補間動畫屬性動畫記憶體洩露分析

在使用屬性動畫的時候,我們知道如果不在頁面結束的時候釋放掉動畫,就會引起記憶體洩露。 簡單的說就是ValueAnimator在AnimationHandler註冊自己的AnimationFrameCallback,AnimationFrameCallback介面

一個JAVA單例模式的典型錯誤應用的分析解決方法

                問題來自論壇,其程式碼如下:[java] view plain copy print?import java.sql.Connection;  import java.sql.PreparedStatement;  import java.sql.ResultSet;  imp

詳解java記憶體洩露如何避免記憶體洩漏

源地址:http://www.xttblog.com/?p=518 一直以來java都佔據著語言排行榜的頭把交椅。這是與java的設計密不可分的,其中最令大家喜歡的不是面向物件,而是垃圾回收機制。你只需要簡單的建立物件而不需要負責釋放空間,因為Java的垃圾回收器會負責記憶

[JAVA]Apache FTPClient操作“卡死”問題的分析解決

1 import org.apache.commons.net.ftp.FTP; 2 import org.apache.commons.net.ftp.FTPClient; 3 import org.apache.commons.net.ftp.FTPFile; 4 import org.a

HBase中正則過濾表示式與JAVA正則表示式不一致問題的分析解決

HBase提供了豐富的查詢過濾功能。 比如說它提供了RegexStringComparator這樣的函式,可以實現按照正則表示式進行過濾。它可以有效地彌補向前綴查詢這樣的機制,從而可以使hbase也

記憶體溢位之PermGen OOM深入分析解決方案

閱讀原文 *現在,網上關於討論PermGen OOM的資料很多,但是深入分析PermGen區域記憶體溢位原因的資料很少。本篇文章嘗試全面分析一下PermGen OOM的原因,其中涉及到了Java虛擬機器執行時資料區、型別裝載、型別解除安裝等,測試程式碼涉及到了JMX協議。

京東某系統雙十一記憶體飆升分析解決方案

一、問題現象 系統在雙十一期間出現頻繁記憶體飆升現象,記憶體在幾天內直接飆升到報警閾值。在堆記憶體空間由3g調整到4g後,依然出現堆記憶體超過閾值問題,同時在重啟若干次記憶體依然會飆升堆積。同時也發現了jvm一直不出現full gc,young gc稍微有點顫

JAVA]Apache FTPClient操作“卡死”問題的分析解決

最近在和一個第三方的合作中不得已需要使用FTP檔案介面。由於FTP Server由對方提供,而且雙方背後各自的網路環境環境都很不單純等等原因,造成測試環境無法模擬實際情況。測試環境中程式一切正常,但是在部署到生產環境之後發現FTP操作不規律性出現“卡死”現象:程式捕獲不到任何

Java記憶體洩露的一個小例子

Java記憶體洩露     一般來說記憶體洩漏有兩種情況。一種情況如在C/C++語言中的,在堆中的分配的記憶體,在沒有將其釋放掉的時候,就將所有能訪問這塊記憶體的方式都刪掉(如指標重新賦值);另一種情況則是在記憶體物件明明已經不需要的時候,還仍然保留著這塊記憶體和它的訪問方式(引用)。第一種情況,在Ja

Android JNI呼叫OpenCV,長時間執行記憶體異常,導致閃退的log分析解決---(ReferenceTable overflow (max=1024)造成的)

首先交代下問題背景,前些日子自己在android上搞了個入侵檢測玩。就是camera當作監控裝置,每隔一定時間檢測是否有東西入侵,如果入侵率到一定程度就報警的東西。最近發現,每次執行超過20分鐘,app直接掛掉消失。下面附上核心完整log:01-01 21:17:42.321

資料庫連線池記憶體洩漏問題的分析解決方案

## 一、問題描述 上週五晚上主營出現部分裝置掉線,經過檢視日誌發現是由於快取系統出現長時間gc導致的。這裡的gc日誌的特點是: - 1.gc時間都在2s以上,部分節點甚至出現12s超長時間gc。 - 2.同一個節點距離上次gc時間間隔為普遍為13~15天。 ![](https://user-gold-cd

  挖礦程序minerd,wnTKYg入侵分析解決

linux wntkyg minerd 挖礦程序minerd,wnTKYg入侵分析和解決 作者:CYH一.起因:最近登陸一臺redis服務器 發現登陸的時間非常長,而且各種命令敲大顯示出的內容延遲

【轉載】TCP粘包問題分析解決(全)

刪除 而且 實例 報文 底層 nagle 存在 ngxin 想想 TCP通信粘包問題分析和解決(全) 在socket網絡程序中,TCP和UDP分別是面向連接和非面向連接的。因此TCP的socket編程,收發兩端(客戶端和服務器端)都要有成對的socket,因此,發送端為了將

問題的分析解決-思維能力或者思維模式

事情 分類 分層 tro 邏輯推理 預測 我們 本質 以及 原因分析:抓住事物本質。邏輯推理:把握事物之間的聯系,以及分清事物內部的邏輯關系思維策略:分層分類(金字塔那種感覺),反映為解決問題的策略結果預測:對事物發展趨勢做出判斷(對掌握的信息進行分析之後)抓住事物的本質

理解解決Java並發修改異常ConcurrentModificationException(轉載)

word http clas current list show -a next() -s 原文地址:https://www.jianshu.com/p/f3f6b12330c1 理解和解決Java並發修改異常ConcurrentModificationException