`

编码最佳实践(5)--小心!这只是冰山一角

    博客分类:
  • java
阅读更多
    本期的案例依然是来自实际项目,很寻常的代码,却意外遭遇传说中的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.

   同样的,在java中还有一个非常类似的案例,来自最常见的String类,它的substring()方法和split()方法,大家可以翻开jdk 的源码看到具体代码。原理和sublist()方法非常类似,就不重复解释了。

   简单给出一段代码,演示一下substring()方法在类似情景下是如何OOM的:

public class SubstringTest {
        public static void main(String[] args) {
                List<String> cache = new ArrayList<String>();

                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<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

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

    public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<E>(this, fromIndex, toIndex) :
                new SubList<E>(this, fromIndex, toIndex));
    }

    3. RandomAccessSubList的代码,继承自SubList

class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
    RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
        super(list, fromIndex, toIndex);
    }

    public List<E> subList(int fromIndex, int toIndex) {
        return new RandomAccessSubList<E>(this, fromIndex, toIndex);
    }
}
  • 大小: 37 KB
分享到:
评论
2 楼 liubey 2014-02-20  
你这个代码是sublist后仍然一直持有这个sub的引用,一般方法的局部变量就不存在这种问题了
1 楼 mayday85 2012-09-06  
碰巧看过实现,很多返回接口的方法其实都是乱七八糟的子类
比如Google的Lists.transform()
返回的List调用Collections.shuffle(list);时会报错
需要留心稍微看看源码

相关推荐

    google编码规范-2018-6-英文版

    以上只是规范的冰山一角,完整的Google编码规范涵盖了更多细节,如错误处理、代码组织、测试策略等。对于任何开发者来说,理解和遵循这些规范都是提升代码质量的重要步骤。通过遵守统一的编码风格,团队成员能更快地...

    Ruby-Ruby技巧惯用Ruby重构和最佳实践

    以上所述,只是Ruby编程中冰山一角。通过不断学习和实践,你可以掌握更多的Ruby技巧,编写出更加优雅、高效的代码。在开发过程中,遵循最佳实践和代码风格指南,可以提升代码质量和团队协作效率。

    ORACLE DB升级性能保障利器SPA最佳实践

    - 他还是ChinaUnix BLOG专家,曾连续四届获得ITPUB最佳精华和最佳版主。 - 丁俊是电子工业出版社的终身荣誉作者,并且是《剑破冰山-Oracle开发艺术》的副主编。 以上知识点是基于文件中提供的内容综合整理出的...

    張祖榮涉貪案非冰山一角

    張祖榮涉貪案非冰山一角

    CSS3+SVG实现的海平面浮动冰山一角动画效果源码.zip

    在本资源中,"CSS3+SVG实现的海平面浮动冰山一角动画效果源码.zip" 提供了一个利用现代Web技术来创建动态视觉效果的例子。这个项目的核心是结合了CSS3和SVG(可缩放矢量图形)的技术,用于生成一个海平面上冰山浮动...

    Arduino 入门到精通 例程1-Hello World!

    在本文中,我们将深入探讨...这仅仅是Arduino编程的冰山一角,随着你对平台的进一步探索,你将能创建更复杂的项目,实现更多令人惊叹的功能。所以,继续学习,深入理解Arduino,你会发现这是一个充满无限可能的世界。

    剑破冰山--Oracle开发艺术配书源代码

    剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术...

    hyxxsfwy#FMZ-Strategies#Python版冰山委托 - 卖出1

    策略名称Python版冰山委托 - 卖出策略作者小小梦策略描述教学策略,相关文章地址:

    vim命令详细文档

    这些只是Vim命令的冰山一角,Vim的强大在于它的可扩展性和高度定制性。熟练掌握Vim能极大提高文本编辑效率,是每个Linux用户的必备技能之一。通过不断练习和探索,你可以找到适合自己的工作流,享受Vim带来的高效...

    GEE案例-利用Sentinel-1影像数据自动检测和跟踪中型冰山监测(阿蒙森海冰山).pdf

    ### GEE案例—利用Sentinel-1影像数据自动检测与跟踪中型冰山监测(阿蒙森海冰山) #### 研究背景与意义 南极冰山的监测对于理解南大洋海洋、大气以及海冰之间的相互作用至关重要。尽管巨型冰山一直是遥感研究的...

    机器文明的冰山一角:人工智能,孔子与苏格拉底

    机器文明的冰山一角:人工智能,孔子与苏格拉底

    【数学建模】模拟冰山运输系统含Matlab源码.zip

    【数学建模】模拟冰山运输系统含Matlab源码.zip是一个包含Matlab源代码的压缩包,专门用于建立和分析冰山...通过对源码的阅读和实践,不仅可以理解冰山运输系统的运作机制,还能掌握用Matlab解决实际问题的方法和技巧。

    论文研究 - 从生物节律到默认模式网络:心灵冰山一角下隐藏着什么?

    我们有意识的日常自我通常被描述为更大的认知系统的“冰山一角”。 水的边缘将现象的自我与潜伏在其下的潜意识/潜意识分开。 类似于冰山,水下的无意识活动大大超过其上方的有意识活动。 浑浊的水面正好位于下面,这...

    最强大脑数字记忆编码

    然而,这只是数字记忆编码的冰山一角。实际上,每个个人都可以根据自己的喜好和熟悉的事物来设计个性化的编码系统,这样不仅增强了记忆的乐趣,还提升了记忆的准确性。例如,如果一个人对音乐特别感兴趣,他可以选择...

    冰山模型-思维导图-mindmaster.emmx

    冰山模型。文档是采用思维导图的形式记录的,需要用mindmaster打开。 参考资料:...

    Unknown - Unknown - 冰山指令1

    冰山指令是一种特殊的交易策略,主要适用于机构投资者在大规模买卖证券时使用,旨在平衡市场流动性与价格波动的风险。这种指令的目的是在不完全暴露实际交易规模的情况下,吸引其他交易对手参与,以降低对市场价格的...

    常用vim命令合集+资源共享

    以上只是vim命令的冰山一角,vim有着极其丰富的功能和高度的可扩展性,通过学习更多的插件和配置,可以极大地提高编程和文本编辑效率。对于初学者来说,熟练掌握这些基本命令是进一步深入vim的关键。

    干货!玩高频量化,你不得不知道的“冰山算法”!.pdf

    干货!玩高频量化,你不得不知道的“冰山算法”!

Global site tag (gtag.js) - Google Analytics