慎用List中的subList方法

推薦一個(gè)程序員開(kāi)發(fā)、學(xué)習(xí)的好網(wǎng)站,www.it123.top?

歡迎大家轉(zhuǎn)發(fā)收藏。


本期的案例依然是來(lái)自實(shí)際項(xiàng)目,很尋常的代碼,卻意外遭遇傳說(shuō)中的Java"內(nèi)存溢出"。

先來(lái)看看發(fā)生了什么,代碼邏輯很簡(jiǎn)單,在請(qǐng)求的處理過(guò)程中:

1. 創(chuàng)建了一個(gè)ArrayList,然后往這個(gè)list里面放了一些數(shù)據(jù),得到了一個(gè)size很大的list

List cdrInfoList = new ArrayList();

for(...) {

cdrInfoList.add(cdrInfo);

}

2. 從這個(gè)list里面,取出一個(gè)size很小的sublist(我們忽略這里的業(yè)務(wù)邏輯)

cdrSublist = cdrInfoList.subList(fromIndex, toIndex)

3. 這個(gè)cdrSublist被作為value保存到一個(gè)常駐內(nèi)存的Map中(同樣我們忽略這里的業(yè)務(wù)邏輯)

cache.put(key, cdrSublist);

4. 請(qǐng)求處理結(jié)果,原有的list和其他數(shù)據(jù)被拋棄

正常情況下保存到cdrSublist不是太多,其內(nèi)存消耗應(yīng)該很小,但是實(shí)際上sig的同事們?cè)谟肑MAP工具檢查SIG的內(nèi)存時(shí),卻發(fā)現(xiàn)這 里的subList()方法生成的RandomAccessSubList占用的內(nèi)存高達(dá)1.6G! 完全不合符常理。

我們來(lái)細(xì)看subList()和RandomAccessSubList在這里都干了些什么:詳細(xì)的代碼實(shí)現(xiàn)追蹤過(guò)程請(qǐng)見(jiàn)附錄1,我們來(lái)看關(guān)鍵代碼,類(lèi)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. 保存一個(gè)原始list對(duì)象的引用

2. 用offset和size來(lái)表明當(dāng)前sublist的在原始list中的范圍

為了讓大家有一個(gè)感性的認(rèn)識(shí),我們用debug模式跑了一下測(cè)試代碼,截圖如下:

?

可以看到生成的sublist對(duì)象內(nèi)有一個(gè)名為"l"的屬性,這是一個(gè)ArrayList對(duì)象,注意它的id和原有的list對(duì)象相同(圖中都是id=33)。

這種實(shí)現(xiàn)方式主要是考慮運(yùn)行時(shí)性能,可以比較一下普通的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)需要?jiǎng)?chuàng)建新的list對(duì)象,然后添加所需內(nèi)容,相比之下無(wú)論是內(nèi)存消耗還是運(yùn)行效率都不如前面SubList直接引用原始 list+記錄偏差量的方式。

但是SubList的這種方式,會(huì)有一個(gè)極大的隱患:這個(gè)SubList的實(shí)例中,保存有原有l(wèi)ist對(duì)象的引用——而且是強(qiáng)引用,這意味著, 只要sublist沒(méi)有被jvm回收,那么這個(gè)原有l(wèi)ist對(duì)象就不能gc,這個(gè)list中保存的所有對(duì)象也不能gc,即使這個(gè)list和其包含的對(duì)象已經(jīng)沒(méi)有其他任何引用。

這個(gè)就是Java世界中“內(nèi)存泄露"的一個(gè)經(jīng)典實(shí)例:某些被期望能被JVM回收的對(duì)象,卻因?yàn)槟硞€(gè)沒(méi)有被覺(jué)察到的角落中"偷偷的"保留 了一個(gè)引用而躲過(guò)GC......在SIG的這個(gè)例子中,我們本來(lái)只想在內(nèi)存中保留很少很少的一點(diǎn)點(diǎn)數(shù)據(jù),被意外的將整個(gè)list和它包含的所 有對(duì)象都留下來(lái)。注意在截圖中,list的size為100000,而sublist只是1而已,這就是我們標(biāo)題中所說(shuō)的"冰山一角"。

這里有一段實(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());

}

}

}

在我的測(cè)試中,打印結(jié)果為"cache size = 121",也就是說(shuō)我的測(cè)試中121個(gè)list,每個(gè)list里面只放了一個(gè)Integer對(duì)象,就可以吃 掉所有內(nèi)存,造成out of memory.

仔細(xì)的同學(xué)會(huì)發(fā)現(xiàn),其實(shí)在sublist()方法的javadoc里面,已經(jīng)對(duì)此有明確的說(shuō)明,“The returned list is backed by this list” ,因此提醒大家在使用某個(gè)不熟悉的方法之前最好讀一讀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中還有一個(gè)非常類(lèi)似的案例,來(lái)自最常見(jiàn)的String類(lèi),它的substring()方法和split()方法,大家可以翻開(kāi)jdk 的源碼看到具體代碼。原理和sublist()方法非常類(lèi)似,就不重復(fù)解釋了。

簡(jiǎn)單給出一段代碼,演示一下substring()方法在類(lèi)似情景下是如何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();

}

}

這一次,我的測(cè)試用只用了994個(gè)長(zhǎng)度為1的字符串,就"成功"達(dá)到了OOM。

最后談一下怎么解決上面的問(wèn)題,當(dāng)然前提是我們有需要將得到的小的list或者string長(zhǎng)時(shí)間存放在內(nèi)存中:

1. 對(duì)于sublist()方法得到的list,貌似沒(méi)有太好的辦法,只能用最直接的方式:自己創(chuàng)建新的list,然后將需要的內(nèi)容添加進(jìn)去

2. 對(duì)于substring()/split()方法得到的string,可以用String類(lèi)的構(gòu)造函數(shù)new String(String original)來(lái)創(chuàng)建一個(gè)新的String,這 樣會(huì)重新創(chuàng)建底層的char[]并復(fù)制需要的內(nèi)容,不會(huì)造成"浪費(fèi)"。

String類(lèi)的構(gòu)造函數(shù)new String(String original)是一個(gè)非常特別的構(gòu)造函數(shù),通常沒(méi)有必要使用,正如這個(gè)函數(shù)的javadoc所言 :Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable. 除非明確需要原始字符串的拷貝,否則沒(méi)有必要使用這個(gè)構(gòu)造函數(shù),因?yàn)镾tring是不可變的。

但是對(duì)于前面的這種特殊場(chǎng)景(從超大字符串中substring()得到后再放置到常駐內(nèi)存的結(jié)構(gòu)中),new String(String original)就 可以將我們從這種潛在的內(nèi)存溢出(或者浪費(fèi))中拯救出來(lái)。因此,當(dāng)遇到同時(shí)處理大字符串+長(zhǎng)時(shí)間放置內(nèi)容在內(nèi)存中時(shí),請(qǐng)小心。

最后鳴謝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類(lèi)的subList()函數(shù)的代碼,對(duì)于A(yíng)rrayList,返回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);

}

}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容