内存充裕下的OOM (二), StringBuilder和均摊分析

/*

*author: ahuaxuan

*date: 2010-05-14

*/

介绍:

在前面的一篇文章中http://ahuaxuan.iteye.com/blog/662629, ahuaxuan遇到了一个在内存相对充裕的情况下发生OOM的场景,具体的文章见:

然后时隔不久,该问题再次出现,但是出现在不同的场景下,现象有非常雷同之处.但是这次更离谱:

在某个他人的项目中, eden区还有300m空间的时候发生了OOM.

而在前面一篇文章中,ahuaxuan揭示了在jackrabbit原有的搜索模块中出现的问题,其中主要的问题是大的char[]对象需要扩容的时候,虽然在这个项目中和lucene没有任何关系,但是问题表现出来的现象非常相似,不出意外的话,应该也是大的char[]或者byte[]达到极限所带来的问题. 思考一下,我们常见的char[]的应用有哪些, 无非就是StringBuilder,StringBuffer.

从他的日志来看,问题主要出现在StringBuilder的expandCapacity方法上,看到log里的expandCapacity, ahuaxuan马上知道问题所在了.(问题很简单,一说大家就都明白了,但是为了显示出我们的学问,我们也要适时的更加深入一点,不能这么草草了事,是吧,开个玩笑),我们知道StringBuilder内部其实是维护了一个

<span style="font-size: medium;">/**
* The value is used for character storage.
*/
char value[];</span>

而数组是无法扩容的,所以当数组空间不够的时候,将会创建一个更大的数组,然后把原来数组的数据全部都拷贝到新的数组中:

<span style="font-size: medium;">void expandCapacity(int minimumCapacity) {
int newCapacity = (value.length + 1) * 2;
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if (minimumCapacity > newCapacity) {
newCapacity = minimumCapacity;
}
value = Arrays.copyOf(value, newCapacity);
}</span>

接着再把新的数据追加到到新的char数组的后面.

<span style="font-size: medium;">public AbstractStringBuilder append(char str[]) {
int newCount = count + str.length;
if (newCount > value.length)
expandCapacity(newCount);
System.arraycopy(str, 0, value, count, str.length);
count = newCount;
return this;
}
</span>

原理很简单,这里也许你要问,这样是不是性能的消耗很大??

其实这里有两个考虑:

1.如果算单次扩容,那么确实这里的代价会比较大.

2.如果算在整个StringBuilder的生命周期中,那么这个扩容操作只占整个生命周期的一小部分.

它的理论基础就是均摊理论,也就是说如过把这次扩容操作所消耗的代价平均分配到每次对这个StringBuilder的操作上,那么这个平均下来的代价的增长是微小的.

如果听到ahuaxuan上面山寨的解释不足以让你理解的话, 那这里再引用一下书上的原话来解释一下均摊的问题:

写道

均摊分析是对一组操作的分析。特别是均摊分析允许处理这样一种情况, n个操作在最差情况下的代价小于任何一个操作在最差情况下的代价的n倍。均摊分析并不是把注意力集中到每个操作的单独代价,然后再把他们加起来,而是看到整个一组操作的代价,再把整体的代价分摊到每一个单独的操作上去。

当然,同样的问题也会出现在StringBuffer, ArrayList等等我们常见的类中.

下面我们来详细考量一下该理论在StringBuilder类的空间复杂度和时间复杂度. 假设我们有100个char需要放到一个stringbuilder中,根据它的实现,一共会有3次”扩容”, 并且有100次append操作, 假设一次”扩容”(建更大的数组,然后执行拷贝) , 假设扩容的时间消耗是m, append操作的时间消耗是n, 那么我们在这次StringBuilder的使用过程中,总的时间消耗是cost= (3*m + 100 * n) / 100. 而且m >> n. 所以我们在使用StringBuilder的过程中,要思考的是如何降低cost的值.当然降低3 * m是最好的, 但是对于m我们无能为力, 那么就对’3’下手把, 如果我们已知我们char的总数,我们就可以把这个3降下来,只要通过StringBuilder sb = new StringBuilder(100),这样就可以避免了3次扩容, 这样cost = (100 * n) / 100 = n.

这时间问题是我们避免3次扩容的最佳理由么, 不是! 为什么,因为看上去 (3*m + 100 * n) / 100 并不比 n高到哪里去,所以在ahuaxuan的case中,性能并不是这样的差. 既然时间复杂度还不是最佳理由, 我们的目光自然而然的转到空间复杂度上.

其实刚才书上的均摊定义对于StringBuilder之类的实现来说只解释了时间复杂度的问题,但是并没有涉及到空间复杂度. 由于我们并不能直接操作内存的分配,所以在jvm中问题要显得更加神秘一点(一般的java程序远对于计算机科学的理解还是非常有待提高的).

继续上面一个case, 第一次扩容时, char[16]不够,重新新建了一个 char[(16+1)*2], 第二次扩容char[(34 + 1) * 2], 第三次扩容char[(70 + 1)*2].这个时候一共产生了4个数组对象, char[16], char[34], char[70], char[142].其实我们只是append了100个char而已,我们消耗的空间最大却有可能达到262个char.

262个char还不至于消耗多少空间,因为我们只做了100次append, 如果我们有1000k的char呢.

我们来算一下时间的消耗和空间消耗.总的扩容次数为16(1000k个char的情况下)

时间消耗: (16 * m + 1000k * n )/1000k

空间消耗: 34 + 70 + ..... + 294910 + 589822 + 1179646 = 2359244

再整个操作的生命周期内总共会消耗2359244个char的空间,但是由于我们的gc作用,所以同一时刻,我们最大的消耗至少为 589822+ 1179646 = 1769468. 空间消耗几乎至少翻倍(为啥是至少? 因为前面创建的char[]如果还没有被回收,那么消耗的空间就会更大,最大的会达到 2359244个char.如果是多线程在做这样的事情,那么消耗的空间数还要乘以线程数,比如说原来一条线程这样的操作只浪费了一个2M, 但是100条线程其实就浪费了200M这样的空间)

再深入考虑, 难道我们这样做只是增加了空间的消耗吗? 绝不是. 我们还可以从JVM的角度再来考虑一下这个问题. 前面15个char[]的创建,对于jvm的young区的拷贝算法来说也是个不必要的负担,因为如果我们有100个线程在做这个任务,同一时间可能产生15 * 100 = 1500个多余的对象.在gc的时候,这些对象需要mark, copy(前面临时的charp[]至少要copy一次,也就是说它们必须进入一次from or to space),其实在一些场景下,这种操作完全避免,避免的手段就是给StringBuilder一个合理的值.

如果你恰好看过我的第一篇”内存充裕下的OOM”,那么也不难理解在某些情况下(比如说200M以上的文本, 想象一下最后的一次扩容需要多少的申请多少的容量??)StringBuilder也会出现”内存充裕下的OOM”了.

相信看了ahuaxuan的这篇文章之后,在使用StringBuilder的时候你会更胸有成竹了. 同样,在使用ArrayList, StringBuffer, 还有HashMap(不但要数组翻倍,还要resize)之类的实现时也有同样的问题.

总结, 本文ahuaxuan主要解释了以下问题.

在StringBuilder之类的实现在扩容的时候, 带来的

1. 时间消耗.

2. 空间消耗.

3. 对jvm的影响.

通过这几个方面的剖析可以让我们更好的使用java SDK中类似的实现类.写出性能更高,更健壮的代码.

当然古人说授之以鱼不如授之以鱼, 事实上凡是以数组作为数据结构实现的类中,基本上都存在均摊的问题,同时也都存在对JVM有不必要影响的因素,所以需要大家更多的深入理解我们常用的类.

to be continue

时间: 2024-09-04 03:12:54

内存充裕下的OOM (二), StringBuilder和均摊分析的相关文章

内存充裕下的OOM (一) 令人惊叹的rootcause

/* @author: ahuaxuan @date: 2010-4-30 / 在内存充裕的情况下的OOM 理解本文的前提是理解JVM的内存模型:包括 perm, old, young(eden, from(s0), to(s1)), 然后理解young中的垃圾搜集算法(拷贝算法,尤其是eden, from(s0), to(s1)它们分别扮演什么样的角色,为什么任意时间中from和to中必须要有一个是空的), old里的垃圾搜索算法(常见的有:标记压缩算法). 正文: 最近一台测试服务器抛出了O

android程序数据保存在单例模式中,保存在内存中,会oom吗

问题描述 android程序数据保存在单例模式中,保存在内存中,会oom吗 ```package com.amt.appstore.cache; import java.util.ArrayList; import java.util.List; import android.content.pm.PackageInfo; import com.amt.appstore.download.DownItem; import com.amt.appstore.model.AboutFirm; imp

Android编程之内存溢出解决方案(OOM)实例总结_Android

本文实例总结了Android编程之内存溢出解决方案(OOM).分享给大家供大家参考,具体如下: 在最近做的工程中发现加载的图片太多或图片过大时经常出现OOM问题,找网上资料也提供了很多方法,但自己感觉有点乱,特此,今天在不同型号的三款安卓手机上做了测试,因为有效果也有结果,今天小马就做个详细的总结,以供朋友们共同交流学习,也供自己以后在解决OOM问题上有所提高,提前讲下,片幅有点长,涉及的东西太多,大家耐心看,肯定有收获的,里面的很多东西小马也是学习参考网络资料使用的,先来简单讲下下: 一般我们

Android编程之内存溢出解决方案(OOM)实例总结

本文实例总结了Android编程之内存溢出解决方案(OOM).分享给大家供大家参考,具体如下: 在最近做的工程中发现加载的图片太多或图片过大时经常出现OOM问题,找网上资料也提供了很多方法,但自己感觉有点乱,特此,今天在不同型号的三款安卓手机上做了测试,因为有效果也有结果,今天小马就做个详细的总结,以供朋友们共同交流学习,也供自己以后在解决OOM问题上有所提高,提前讲下,片幅有点长,涉及的东西太多,大家耐心看,肯定有收获的,里面的很多东西小马也是学习参考网络资料使用的,先来简单讲下下: 一般我们

VS2013下动态数组二维数组读二进制文件的问题

问题描述 VS2013下动态数组二维数组读二进制文件的问题 int samples_to_read = 7200; uint8_t **caculate_a; int count2,count3; caculate_a = (uint8_t **)malloc(sizeof(uint8_t *)* 3); for (count1 = 0; count1<3; count1++){ //动态数组分配空间 caculate_a[count1] = (uint8_t *)malloc(sizeof(u

叫停支付宝线下条码(二维码)支付

今日央行支付结算司下发<中国人民银行支付结算司关于暂停支付宝公司线下条码(二维码)支付等业务意见的函>红头文件,叫停支付宝线下条码(二维码)支付,以及虚拟信用卡业务. 文件中称,将条码(二维码)应用于支付领域有关技术,终端的安全标准尚不明确.相关支付撮合验证方式的安全性尚存质疑,存在一定得支付风险隐患.虚拟信用卡突破了现有信用卡业务模式,在落实客户身份识别义务.保障客户信息安全等方面尚待进一步研究. 央行要求杭州中心支行支付结算处全面评估线下条码(二维码)支付.虚拟信用卡的合规性和安全性,并于

java-Android-JAVA程序分析,帮忙分析下以下程序如何拆行,怎么分析。

问题描述 Android-JAVA程序分析,帮忙分析下以下程序如何拆行,怎么分析. public class ja { private Bundle metaData; public void doit() { String str3 = config("sapi_sign", "e56b4eb0473d219c5317afb7ccf66e8f"); System.out.println(str3); } private String config(String p

分享下今天研究的流量上限DDos攻击分析和解决方案

分享下今天研究的流量上限DDos攻击分析和解决方案   经常听到或者碰到某个网站被攻击,一般都是流量攻击.今天自己写了个程序测下相关的上限,程序只简单做了个get html操作(不包含图片等资源文件). 用一台双核CPU机器A,启100个线程,连续发送服务器B,统计出的结果是每秒钟发173个请求,机器A的发送带宽450Kbps,机器A的接收带宽2.8Mbps,机器B的发送带宽2.8Mbps,机器B的接收带宽450Kbps. 用一台双核CPU机器A,启1000个线程,连续发送服务器B,统计出的结果

云计算下MapReduce多组容错机制架构的分析与研究

云计算下MapReduce多组容错机制架构的分析与研究 张治斌   李燕歌 提出了MapReduce多组容错机制,在传统的Hadoop MapReduce架构上进行改进,即在同机柜中的Task-T racker节点之间增加了多组关系,这样可以缩短发现失效节点的时间,同时减轻JobT racker节点的负荷,减低了带宽使用率,减少网络拥塞.通过实验证明,MapReduce多组容错机制提高了M apReduce的工作效率. 云计算下MapReduce多组容错机制架构的分析与研究