本期的案例依然是来自实际项目,很寻常的代码,却意外遭遇传说中的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
分享到:
相关推荐
毓华(十八年风雨,守得冰山雪莲花开) 时间:2010-05-10 地点:重庆航天职业学院 说明:以下五十个语句都按照测试数据进行过测试,最好每次只单独运行一个语句。 问题及描述: --1.学生表 Student(S#,Sname,Sage,Ssex...
FPGA之冰山一角,讲述了FPGA的基本知识和quartus软件的使用。
張祖榮涉貪案非冰山一角
剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术配书源代码 剑破冰山--Oracle开发艺术...
策略名称Python版冰山委托 - 卖出策略作者小小梦策略描述教学策略,相关文章地址:
机器文明的冰山一角:人工智能,孔子与苏格拉底
我们有意识的日常自我通常被描述为更大的认知系统的“冰山一角”。 水的边缘将现象的自我与潜伏在其下的潜意识/潜意识分开。 类似于冰山,水下的无意识活动大大超过其上方的有意识活动。 浑浊的水面正好位于下面,这...
冰山模型。文档是采用思维导图的形式记录的,需要用mindmaster打开。 参考资料:...
CSS3+SVG实现的海平面浮动冰山一角动画效果源码.zip
干货!玩高频量化,你不得不知道的“冰山算法”!
车联网仅显露冰山一角 运营商车厂合作“催化”产业成长.pdf
linux常用命令大全这些只是Linux中可用命令的冰山一角。每个命令通常都有许多选项和参数,可以用于执行更复杂的任务。要获取特定命令的详细信息,
隐秘的对冲基金王者,起底文艺复兴黑箱之内的冰山一角.doc
3. 集合竞价阶段冰山指令的执行8 4. 总结和讨论9 2. 盘中连续交易阶段冰山指令的执行 2. 盘中连续交易阶段冰山指令的执行
[案例]【冰山一角】商业创业计划书优化版PPT模版[策划&调研].ppt
剑破冰山-Oracle开发艺术].卢涛等.扫描版[www.ed2kers.com].pdf.剑破冰山-Oracle开发艺术].卢涛等.扫描版[www.ed2kers.com].pdf.
一个用mathematica程序实现冰山运输的问题,有很大价值,很不错。
剑破冰山-oracle开发技术pdf+源代码剑破冰山-oracle开发技术pdf+源代码剑破冰山-oracle开发技术pdf+源代码
剑破冰山:Oracle开发艺术-数据库编程规范
介绍您最喜欢的新扩展-惊人的冰山! 创建此扩展名是为了提升您的浏览体验。 每次您打开新标签页或首页时,冰山的新图像都会显示为可自定义页面的墙纸,供您欣赏,以及更多独特功能! 它不仅是带有惊人冰山图像的墙纸...