首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 移动开发 > 移动开发 >

(转载)String种的split方法引起的内存泄漏

2012-06-27 
(转载)String类的split方法引起的内存泄漏转载:原文地址:http://jarfield.iteye.com/admin/blogs/583946?

(转载)String类的split方法引起的内存泄漏

转载:原文地址:http://jarfield.iteye.com/admin/blogs/583946

?

一直赞叹Sun对待技术的严谨和优雅(可怜的Sun)。Sun JDK中Java库的源代码,连注释都清清楚楚、规规范范,javadoc注解的使用也一丝不苟,读起来很熟舒服。因此,在日常工作和学习中,经常读读Java库的源代码,不亦乐乎?如果遇到诡异问题,源代码的帮助就更大了。

?

??? 闲话少说,回归正题。这几天,一直在为Java的“内存泄露”问题纠结。Java应用程序占用的内存在不断的、有规律的上涨,最终超过了监控阈值。福尔摩斯不得不出手了!

?

??? 说起Java的内存泄露,其实定义不是那么明确。首先,如果JVM没有bug,那么理论上是不会出现“无法回收的堆空间”,也就是说C/C++中的那种内存泄露在Java中不存在的。其次,如果由于Java程序一直持有某个对象的引用,但是从程序逻辑上看,这个对象再也不会被用到了,那么我们可以认为这个对象被泄露了。如果这样的对象数量很多,那么很明显,大量的内存空间就被泄露(“浪费”更准确一些)了。

?

??? 不过,本文要说的内存泄露,并不属于上述原因,因此打上了引号。其具体原因,确实出乎意料。欲知详情,请看下面讲解。

分析内存泄露的一般步骤

?

??? 如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析

    把Java应用程序使用的heap dump下来使用Java heap分析工具,找出内存占用超出预期(一般是因为数量太多)的嫌疑对象必要时,需要分析嫌疑对象和其他对象的引用关系。查看程序的源代码,找出嫌疑对象数量过多的原因。
dump heap

?

??? 如果Java应用程序出现了内存泄露,千万别着急着把应用杀掉,而是要保存现场。如果是互联网应用,可以把流量切到其他服务器。保存现场的目的就是为了把运行中JVM的heap dump下来。

?

??? JDK自带的jmap工具,可以做这件事情。它的执行方法是:

(转载)String种的split方法引起的内存泄漏

??? 言归正传,我用MAT打开了heap.bin,很容易看出,char[]的数量出其意料的多,占用90%以上的内存。一般来说,char[]在JVM确实会占用很多内存,数量也非常多,因为String对象以char[]作为内部存储。但是这次的char[]太贪婪了,仔细一观察,发现有数万计的char[],每个都占用数百K的内存。这个现象说明,Java程序保存了数以万计的大String对象。结合程序的逻辑,这个是不应该的,肯定在某个地方出了问题。

?

顺藤摸瓜

?

??? 在可疑的char[]中,任意挑了一个,使用Path To GC Root功能,找到该char[]的引用路径,发现String对象是被一个HashMap中引用的。这个也是意料中的事情,Java的内存泄露多半是因为对象被遗留在全局的HashMap中得不到释放。不过,该HashMap被用作一个缓存,设置了缓存条目的阈值,导达到阈值后会自动淘汰。从这个逻辑分析,应该不会出现内存泄露的。虽然缓存中的String对象已经达到数万计,但仍然没有达到预先设置的阈值(阈值设置地比较大,因为当时预估String对象都比较小)。

?

??? 但是,另一个问题引起了我的注意:为什么缓存的String对象如此巨大?内部char[]的长度达数百K。虽然缓存中的String对象数量还没有达到阈值,但是String对象大小远远超出了我们的预期,最终导致内存被大量消耗,形成内存泄露的迹象(准确说应该是内存消耗过多)

?

??? 就这个问题进一步顺藤摸瓜,看看String大对象是如何被放到HashMap中的。通过查看程序的源代码,我发现,确实有String大对象,不过并没有把String大对象放到HashMap中,而是把String大对象进行split(调用String.split方法),然后将split出来的String小对象放到HashMap中了。

?

??? 这就奇怪了,放到HashMap中明明是split之后的String小对象,怎么会占用那么大空间呢?难道是String类的split方法有问题?

?

查看代码

?

??? 带着上述疑问,我查阅了Sun JDK6中String类的代码,主要是是split方法的实现:

Java代码
    public???String[]?split(String?regex,?int?limit)?{??????return?Pattern.compile(regex).split(this,?limit);??}??

可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:

Java代码
    public???String[]?split(CharSequence?input,?int?limit)?{??????????int?index?=?0;??????????boolean?matchLimited?=?limit?>?0;??????????ArrayList<String>?matchList?=?new???ArrayList<String>();??????????Matcher?m?=?matcher(input);??????????//?Add?segments?before?each?match?found??????????while(m.find())?{??????????????if?(!matchLimited?||?matchList.size()?<?limit?-?1)?{??????????????????String?match?=?input.subSequence(index,???m.start()).toString();??????????????????matchList.add(match);??????????????????index?=?m.end();??????????????}?else?if?(matchList.size()?==?limit?-?1)?{?//?last?one??????????????????String?match?=?input.subSequence(index,?????????????????????????????????????????????????????input.length()).toString();??????????????????matchList.add(match);??????????????????index?=?m.end();??????????????}??????????}??????????//?If?no?match?was?found,?return?this??????????if?(index?==?0)??????????????return?new?String[]?{input.toString()};??????????//?Add?remaining?segment??????????if?(!matchLimited?||?matchList.size()?<?limit)??????????????matchList.add(input.subSequence(index,???input.length()).toString());??????????//?Construct?result??????????int?resultSize?=?matchList.size();??????????if?(limit?==?0)??????????????while?(resultSize?>?0?&&???matchList.get(resultSize-1).equals(""))??????????????????resultSize--;??????????String[]?result?=?new?String[resultSize];??????????return?matchList.subList(0,?resultSize).toArray(result);??????}??

??? 注意看第9行:Stirng match = input.subSequence(intdex, m.start()).toString();

这里的match就是split出来的String小对象,它其实是String大对象subSequence的结果。继续看String.subSequence的代码:

Java代码
    public???CharSequence?subSequence(int?beginIndex,?int?endIndex)?{??????????return?this.substring(beginIndex,?endIndex);??}??

??? String.subSequence有调用了String.subString,继续看:

Java代码
    public?String???substring(int?beginIndex,?int?endIndex)?{??????if?(beginIndex?<?0)?{??????????throw?new?StringIndexOutOfBoundsException(beginIndex);??????}??????if?(endIndex?>?count)?{??????????throw?new?StringIndexOutOfBoundsException(endIndex);??????}??????if?(beginIndex?>?endIndex)?{??????????throw?new?StringIndexOutOfBoundsException(endIndex?-?beginIndex);??????}??????return?((beginIndex?==?0)?&&?(endIndex?==?count))???this?:??????????new?String(offset?+?beginIndex,?endIndex?-?beginIndex,?value);??????}??

??? 看第11、12行,我们终于看出眉目,如果subString的内容就是完整的原字符串,那么返回原String对象;否则,就会创建一个新的String对象,但是这个String对象貌似使用了原String对象的char[]。我们通过String的构造函数确认这一点:

Java代码
    //?Package???private?constructor?which?shares?value?array?for?speed.??????String(int?offset,?int?count,?char?value[])?{??????this.value?=?value;??????this.offset?=?offset;??????this.count?=?count;??????}??

??? 为了避免内存拷贝、加快速度,Sun JDK直接复用了原String对象的char[],偏移量和长度来标识不同的字符串内容。也就是说,subString出的来String小对象仍然会指向原String大对象的char[],split也是同样的情况。这就解释了,为什么HashMap中String对象的char[]都那么大。

原因解释

?

??? 其实上一节已经分析出了原因,这一节再整理一下:

    程序从每个请求中得到一个String大对象,该对象内部char[]的长度达数百K。程序对String大对象做split,将split得到的String小对象放到HashMap中,用作缓存。Sun JDK6对String.split方法做了优化,split出来的Stirng对象直接使用原String对象的char[]HashMap中的每个String对象其实都指向了一个巨大的char[]HashMap的上限是万级的,因此被缓存的Sting对象的总大小=万*百K=G级。G级的内存被缓存占用了,大量的内存被浪费,造成内存泄露的迹象。
解决方案

?

??? 原因找到了,解决方案也就有了。split是要用的,但是我们不要把split出来的String对象直接放到HashMap中,而是调用一下String的拷贝构造函数String(String original),这个构造函数是安全的,具体可以看代码:

Java代码
    ????/**??????*?Initializes?a?newly?created?{@code?String}?object?so?that?it??represents??????*?the?same?sequence?of?characters?as?the?argument;?in?other?words,??the??????*?newly?created?string?is?a?copy?of?the?argument?string.?Unless?an??????*?explicit?copy?of?{@code?original}?is?needed,?use?of?this??constructor?is??????*?unnecessary?since?Strings?are?immutable.??????*??????*?@param??original??????*?????????A?{@code?String}??????*/??????public?String(String?original)?{??????int?size?=?original.count;??????char[]?originalValue?=?original.value;??????char[]?v;??????if?(originalValue.length?>?size)?{??????????//?The?array?representing?the?String?is?bigger?than?the?new??????????//?String?itself.??Perhaps?this?constructor?is?being?called??????????//?in?order?to?trim?the?baggage,?so?make?a?copy?of?the?array.??????????????int?off?=?original.offset;??????????????v?=?Arrays.copyOfRange(originalValue,?off,?off+size);??????}?else?{??????????//?The?array?representing?the?String?is?the?same??????????//?size?as?the?String,?so?no?point?in?making?a?copy.??????????v?=?originalValue;??????}??????this.offset?=?0;??????this.count?=?size;??????this.value?=?v;??????}??

??? 只是,new String(string)的代码很怪异,囧。或许,subString和split应该提供一个选项,让程序员控制是否复用String对象的char[]。

是否Bug

?

??? 虽然,subString和split的实现造成了现在的问题,但是这能否算String类的bug呢?个人觉得不好说。因为这样的优化是比较合理的,subString和spit的结果肯定是原字符串的连续子序列。只能说,String不仅仅是一个核心类,它对于JVM来说是与原始类型同等重要的类型。

?

??? JDK实现对String做各种可能的优化都是可以理解的。但是优化带来了忧患,我们程序员足够了解他们,才能用好他们。

一些补充

有个地方我没有说清楚。

?

我的程序是一个Web程序,每次接受请求,就会创建一个大的String对象,然后对该String对象进行split,最后split之后的String对象放到全局缓存中。如果接收了5W个请求,那么就会有5W个大String对象。这5W个大String对象都被存储在全局缓存中,因此会造成内存泄漏。我原以为缓存的是5W个小String,结果都是大String。

?

“抛出异常的爱”同学,在回帖(第7页)中建议用"java.io.StreamTokenizer"来解决本文的问题。确实是终极解决方案,比我上面提到的“new String()”,要好很多很多。

?

热点排行