案例分析:java中substring引發的Full gc

問題定位

由於應用頻繁地Full gc,就dump了內存下來用MAT分析,發現有個map佔用了98%的內存,於是找到這個map

private ConcurrentMap<String, String> nick2numid = new ConcurrentHashMap<String, String>();
存放的是nick與id的映射關係,從MAT中找到map的每一個entry如下圖所示:


這裏解釋一下兩個概念

Shallow Heap:對象佔用了多少內存(單位:字節)

Retained Heap:如果對象被回收會釋放多少內存,也就是對象hold住的內存

map的key是一個String類型,其Shallow Heap爲32byte,Retained Heap爲1104byte。一般對於String類型,具有不可變性,這兩個值應該相等纔對,帶着疑惑找到了問題所在,分步描述如下:

1、從頁面傳了一個參數到後端,這個參數攜帶了cookie的內容,恰好就是1104byte這麼長,不妨設置這個參數爲cookie;

2、後端拿到cookie這個參數後,需要其中的nick的值,採用的是String類的split方法(&做爲分隔符)得到一個數組,其中有一項的值爲nick=xxx

3、將nick的值做爲key放入nick2numid中

nick2numid.put("xxx",id)
最終發現問題出在String類的substring方法上?

分析問題

其實String的split方法上調用了substring方法,先來看看split的源碼實現吧

public String[] split(String regex) {
    return split(regex, 0);
}
public String[] split(String regex, int limit) {
		return Pattern.compile(regex).split(this, limit);
}

 public String[] split(CharSequence input, int limit) {
        ArrayList<String> matchList = new ArrayList<String>();
        Matcher m = matcher(input);

        // Add segments before each match found
        while(m.find()) {
            if (!matchLimited || matchList.size() < limit - 1) {
                String match = input.subSequence(index, m.start()).toString();
                matchList.add(match);
                index = m.end();
            } else if (matchList.size() == limit - 1) { // last one
                String match = input.subSequence(index,
                                                 input.length()).toString();
                matchList.add(match);
                index = m.end();
            }
        }
}   

public CharSequence subSequence(int beginIndex, int endIndex) {
    return this.substring(beginIndex, endIndex);
}
String.split->Pattern.split->subSequence->substring

從以上代碼可以看出String類的split方法確實調用了substring方法
下面來看看substring方法源碼:

private final char value[];

public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
	throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
	throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
	throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
	new String(offset + beginIndex, endIndex - beginIndex, value);
}
注意value這個char數組,存放的是String每個字符的內容,substring直接依賴了這個數組。如果substring產生的字符串沒有被java虛擬機回收,這個char數組也不會被回收。

問題回顧:應用中的nick2numId這個map生命週期很長,只有在用戶退出的時候纔會刪除其中的entry項,恰好這個map非常大,dump內存的時候size已經達到幾十萬,相當於幾十萬個cookie被這個map所hold住,內存被耗盡產生Full gc。

解決方案

當時想到幾種方案:
1、String.intern()?
2、拿到value數組產生一個新數組?
3、採用分佈式緩存來存放nick與id的映射關係?

採用第一種方法是否可行?不可取,原因有2:

intern() 所使用的是一個全局的池,並不需要如此大作用域的緩存;

intern會向常量池中添加內容,持久代空間本來就很小,被nick所佔用可能引起OutOfMemoryError!

後來採用的是第二種方法:new String(nick.toCharArray()),很快上線使內存利用率得到提升(fast,not the best)。

JVM中存放生命週期長的大map始終是一個隱患,說不定哪一天由於map元素過多或者刪除不及時導致OOM,應儘快採用第3種方案:分佈式緩存。nick放入緩存之後,cookie會很快被回收,而且系統的可用內存將會大大提高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章