java list 之 sublist OOM

 本期的案例依然是来自实际项目,很寻常的代码,却意外遭遇传说中的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<E> extends AbstractList<E> { 
    private AbstractList<E> l; 
    private int offset; 
    private int size; 

    SubList(AbstractList<E> 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<E> subList(int fromIndex, int toIndex) { 
        List<E> 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<List<Integer>> cache = new ArrayList<List<Integer>>(); 

                try { 
                        while (true) { 
                                List<Integer> list = new ArrayList<Integer>(); 
                                for (int j = 0; j < 100000; j++) { 
                                        list.add(j); 
                                } 

                                List<Integer> 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. 

 

转载自 

http://skydream.iteye.com/blog/1672745

posted on 2013-11-21 22:06  ROCK.  阅读(914)  评论(0编辑  收藏  举报

导航