推薦一個程序員開發(fā)档插、學(xué)習(xí)的好網(wǎng)站瘟栖,www.it123.top?
歡迎大家轉(zhuǎn)發(fā)收藏拇厢。
本期的案例依然是來自實(shí)際項目矾飞,很尋常的代碼,卻意外遭遇傳說中的Java"內(nèi)存溢出"拯爽。
先來看看發(fā)生了什么顿痪,代碼邏輯很簡單,在請求的處理過程中:
1. 創(chuàng)建了一個ArrayList蘑斧,然后往這個list里面放了一些數(shù)據(jù),得到了一個size很大的list
List cdrInfoList = new ArrayList();
for(...) {
cdrInfoList.add(cdrInfo);
}
2. 從這個list里面须眷,取出一個size很小的sublist(我們忽略這里的業(yè)務(wù)邏輯)
cdrSublist = cdrInfoList.subList(fromIndex, toIndex)
3. 這個cdrSublist被作為value保存到一個常駐內(nèi)存的Map中(同樣我們忽略這里的業(yè)務(wù)邏輯)
cache.put(key, cdrSublist);
4. 請求處理結(jié)果竖瘾,原有的list和其他數(shù)據(jù)被拋棄
正常情況下保存到cdrSublist不是太多,其內(nèi)存消耗應(yīng)該很小花颗,但是實(shí)際上sig的同事們在用JMAP工具檢查SIG的內(nèi)存時捕传,卻發(fā)現(xiàn)這 里的subList()方法生成的RandomAccessSubList占用的內(nèi)存高達(dá)1.6G! 完全不合符常理。
我們來細(xì)看subList()和RandomAccessSubList在這里都干了些什么:詳細(xì)的代碼實(shí)現(xiàn)追蹤過程請見附錄1扩劝,我們來看關(guān)鍵代碼庸论,類SubList的實(shí)現(xiàn)代碼,忽略不相關(guān)的內(nèi)容
class SubList extends AbstractList {
private AbstractList l;
private int offset;
private int size;
SubList(AbstractList list, int fromIndex, int toIndex) {
......
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
}
這里我們可以清楚的看到SubList的實(shí)現(xiàn)原理:
1. 保存一個原始list對象的引用
2. 用offset和size來表明當(dāng)前sublist的在原始list中的范圍
為了讓大家有一個感性的認(rèn)識棒呛,我們用debug模式跑了一下測試代碼聂示,截圖如下:
?
可以看到生成的sublist對象內(nèi)有一個名為"l"的屬性,這是一個ArrayList對象簇秒,注意它的id和原有的list對象相同(圖中都是id=33)鱼喉。
這種實(shí)現(xiàn)方式主要是考慮運(yùn)行時性能,可以比較一下普通的sublist實(shí)現(xiàn):
public List subList(int fromIndex, int toIndex) {
List result = ...; // new a empty list
for(int i = fromIndex; i <= toIndex; i++) {
result.add(this.get(i));
}
return result;
}
這種實(shí)現(xiàn)需要創(chuàng)建新的list對象趋观,然后添加所需內(nèi)容扛禽,相比之下無論是內(nèi)存消耗還是運(yùn)行效率都不如前面SubList直接引用原始 list+記錄偏差量的方式。
但是SubList的這種方式皱坛,會有一個極大的隱患:這個SubList的實(shí)例中编曼,保存有原有l(wèi)ist對象的引用——而且是強(qiáng)引用,這意味著剩辟, 只要sublist沒有被jvm回收掐场,那么這個原有l(wèi)ist對象就不能gc,這個list中保存的所有對象也不能gc抹沪,即使這個list和其包含的對象已經(jīng)沒有其他任何引用刻肄。
這個就是Java世界中“內(nèi)存泄露"的一個經(jīng)典實(shí)例:某些被期望能被JVM回收的對象,卻因?yàn)槟硞€沒有被覺察到的角落中"偷偷的"保留 了一個引用而躲過GC......在SIG的這個例子中融欧,我們本來只想在內(nèi)存中保留很少很少的一點(diǎn)點(diǎn)數(shù)據(jù)敏弃,被意外的將整個list和它包含的所 有對象都留下來。注意在截圖中噪馏,list的size為100000麦到,而sublist只是1而已绿饵,這就是我們標(biāo)題中所說的"冰山一角"。
這里有一段實(shí)例代碼瓶颠,大家可以運(yùn)行一下拟赊,很快就可以看到Java世界中名聲顯赫的OOM:
public class SublistTest {
public static void main(String[] args) {
List> cache = new ArrayList>();
try {
while (true) {
List list = new ArrayList();
for (int j = 0; j < 100000; j++) {
list.add(j);
}
List sublist = list.subList(0, 1);
cache.add(sublist);
}
} finally {
System.out.println("cache size = " + cache.size());
}
}
}
在我的測試中,打印結(jié)果為"cache size = 121"粹淋,也就是說我的測試中121個list吸祟,每個list里面只放了一個Integer對象,就可以吃 掉所有內(nèi)存桃移,造成out of memory.
仔細(xì)的同學(xué)會發(fā)現(xiàn)屋匕,其實(shí)在sublist()方法的javadoc里面,已經(jīng)對此有明確的說明借杰,“The returned list is backed by this list” 过吻,因此提醒大家在使用某個不熟悉的方法之前最好讀一讀Javadoc:
Returns a view of the portion of this list between fromIndex, inclusive, and toIndex, exclusive. (If fromIndex and toIndex are equal, the returned list is empty.) The returned list is backed by this list, so changes in the returned list are reflected in this list, and vice-versa. The returned list supports all of the optional list operations supported by this list.
同樣的,在java中還有一個非常類似的案例蔗衡,來自最常見的String類纤虽,它的substring()方法和split()方法,大家可以翻開jdk 的源碼看到具體代碼绞惦。原理和sublist()方法非常類似逼纸,就不重復(fù)解釋了。
簡單給出一段代碼翩隧,演示一下substring()方法在類似情景下是如何OOM的:
public class SubstringTest {
public static void main(String[] args) {
List cache = new ArrayList();
try {
int i = 1;
while (true) {
String original = buildABigString(i++);
String substring = original.substring(0, 1);
cache.add(substring);
}
} finally {
System.out.println("cache size = " + cache.size());
}
}
private static String buildABigString(int count) {
long thistime = System.currentTimeMillis() + count;
StringBuilder buf = new StringBuilder(1024 * 100);
for(int i = 0; i < 10000; i++) {
buf.append(thistime);
}
return buf.toString();
}
}
這一次樊展,我的測試用只用了994個長度為1的字符串,就"成功"達(dá)到了OOM堆生。
最后談一下怎么解決上面的問題专缠,當(dāng)然前提是我們有需要將得到的小的list或者string長時間存放在內(nèi)存中:
1. 對于sublist()方法得到的list,貌似沒有太好的辦法淑仆,只能用最直接的方式:自己創(chuàng)建新的list涝婉,然后將需要的內(nèi)容添加進(jìn)去
2. 對于substring()/split()方法得到的string,可以用String類的構(gòu)造函數(shù)new String(String original)來創(chuàng)建一個新的String蔗怠,這 樣會重新創(chuàng)建底層的char[]并復(fù)制需要的內(nèi)容墩弯,不會造成"浪費(fèi)"。
String類的構(gòu)造函數(shù)new String(String original)是一個非常特別的構(gòu)造函數(shù)寞射,通常沒有必要使用渔工,正如這個函數(shù)的javadoc所言 :Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable. 除非明確需要原始字符串的拷貝,否則沒有必要使用這個構(gòu)造函數(shù)桥温,因?yàn)镾tring是不可變的引矩。
但是對于前面的這種特殊場景(從超大字符串中substring()得到后再放置到常駐內(nèi)存的結(jié)構(gòu)中),new String(String original)就 可以將我們從這種潛在的內(nèi)存溢出(或者浪費(fèi))中拯救出來。因此旺韭,當(dāng)遇到同時處理大字符串+長時間放置內(nèi)容在內(nèi)存中時氛谜,請小心。
最后鳴謝Ray Tao同學(xué)為本次分享提供素材区端!
附錄:List.sublist() 代碼實(shí)現(xiàn)追蹤
1. ArrayList的代碼值漫,繼承自AbstractList,實(shí)現(xiàn)了RandomAccess接口
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
2. AbstractList類的subList()函數(shù)的代碼织盼,對于ArrayList杨何,返回RandomAccessSubList的實(shí)例
public List subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList(this, fromIndex, toIndex) :
new SubList(this, fromIndex, toIndex));
}
3. RandomAccessSubList的代碼,繼承自SubList
class RandomAccessSubList extends SubList implements RandomAccess {
RandomAccessSubList(AbstractList list, int fromIndex, int toIndex) {
super(list, fromIndex, toIndex);
}
public List subList(int fromIndex, int toIndex) {
return new RandomAccessSubList(this, fromIndex, toIndex);
}
}