慎用List中的subList方法

推荐一个程序员开发、学习的好网站,www.it123.top 

欢迎大家转发收藏。


本期的案例依然是来自实际项目,很寻常的代码,却意外遭遇传说中的Java"内存溢出"。

先来看看发生了什么,代码逻辑很简单,在请求的处理过程中:

1. 创建了一个ArrayList,然后往这个list里面放了一些数据,得到了一个size很大的list

List cdrInfoList = new ArrayList();

for(...) {

cdrInfoList.add(cdrInfo);

}

2. 从这个list里面,取出一个size很小的sublist(我们忽略这里的业务逻辑)

cdrSublist = cdrInfoList.subList(fromIndex, toIndex)

3. 这个cdrSublist被作为value保存到一个常驻内存的Map中(同样我们忽略这里的业务逻辑)

cache.put(key, cdrSublist);

4. 请求处理结果,原有的list和其他数据被抛弃

正常情况下保存到cdrSublist不是太多,其内存消耗应该很小,但是实际上sig的同事们在用JMAP工具检查SIG的内存时,却发现这 里的subList()方法生成的RandomAccessSubList占用的内存高达1.6G! 完全不合符常理。

我们来细看subList()和RandomAccessSubList在这里都干了些什么:详细的代码实现追踪过程请见附录1,我们来看关键代码,类SubList的实现代码,忽略不相关的内容

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的实现原理:

1. 保存一个原始list对象的引用

2. 用offset和size来表明当前sublist的在原始list中的范围

为了让大家有一个感性的认识,我们用debug模式跑了一下测试代码,截图如下:

可以看到生成的sublist对象内有一个名为"l"的属性,这是一个ArrayList对象,注意它的id和原有的list对象相同(图中都是id=33)。

这种实现方式主要是考虑运行时性能,可以比较一下普通的sublist实现:

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;

}

这种实现需要创建新的list对象,然后添加所需内容,相比之下无论是内存消耗还是运行效率都不如前面SubList直接引用原始 list+记录偏差量的方式。

但是SubList的这种方式,会有一个极大的隐患:这个SubList的实例中,保存有原有list对象的引用——而且是强引用,这意味着, 只要sublist没有被jvm回收,那么这个原有list对象就不能gc,这个list中保存的所有对象也不能gc,即使这个list和其包含的对象已经没有其他任何引用。

这个就是Java世界中“内存泄露"的一个经典实例:某些被期望能被JVM回收的对象,却因为某个没有被觉察到的角落中"偷偷的"保留 了一个引用而躲过GC......在SIG的这个例子中,我们本来只想在内存中保留很少很少的一点点数据,被意外的将整个list和它包含的所 有对象都留下来。注意在截图中,list的size为100000,而sublist只是1而已,这就是我们标题中所说的"冰山一角"。

这里有一段实例代码,大家可以运行一下,很快就可以看到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());

}

}

}

在我的测试中,打印结果为"cache size = 121",也就是说我的测试中121个list,每个list里面只放了一个Integer对象,就可以吃 掉所有内存,造成out of memory.

仔细的同学会发现,其实在sublist()方法的javadoc里面,已经对此有明确的说明,“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()方法非常类似,就不重复解释了。

简单给出一段代码,演示一下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的字符串,就"成功"达到了OOM。

最后谈一下怎么解决上面的问题,当然前提是我们有需要将得到的小的list或者string长时间存放在内存中:

1. 对于sublist()方法得到的list,貌似没有太好的办法,只能用最直接的方式:自己创建新的list,然后将需要的内容添加进去

2. 对于substring()/split()方法得到的string,可以用String类的构造函数new String(String original)来创建一个新的String,这 样会重新创建底层的char[]并复制需要的内容,不会造成"浪费"。

String类的构造函数new String(String original)是一个非常特别的构造函数,通常没有必要使用,正如这个函数的javadoc所言 :Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable. 除非明确需要原始字符串的拷贝,否则没有必要使用这个构造函数,因为String是不可变的。

但是对于前面的这种特殊场景(从超大字符串中substring()得到后再放置到常驻内存的结构中),new String(String original)就 可以将我们从这种潜在的内存溢出(或者浪费)中拯救出来。因此,当遇到同时处理大字符串+长时间放置内容在内存中时,请小心。

最后鸣谢Ray Tao同学为本次分享提供素材!

附录:List.sublist() 代码实现追踪

1. ArrayList的代码,继承自AbstractList,实现了RandomAccess接口

public class ArrayList extends AbstractList

implements List, RandomAccess, Cloneable, java.io.Serializable

2. AbstractList类的subList()函数的代码,对于ArrayList,返回RandomAccessSubList的实例

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);

}

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容