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

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

dump heap


  


  


Java代码


1. jmap -dump:format=b,file=heap.bin <pid>  
2.  jmap -dump:format=b,file=xxx.bin pid pid是java程序pid

  

  

  

analyze heap


   将二进制的heap dump文件解析成human-readable的信息,自然是需要专业工具的帮助,这里推荐Memory Analyzer 。


  


  


   言归正传,我用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中 了。


  


查看代码


  


Java代码

1. public  
2. String[] split(String regex, intlimit) {  
3.    returnPattern.compile(regex).split(this, limit);  
4. }  
 
 可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:
 

   Java代码 
 
 
 
1. public  
2. String[] split(CharSequence input, intlimit) {  
3.        intindex = 0;  
4.        booleanmatchLimited = limit > 0;  
5.        ArrayList<String> matchList = new  
6. ArrayList<String>();  
7.        Matcher m = matcher(input);  
8.        // Add segments before each match found 
9.        while(m.find()) {  
10.            if(!matchLimited || matchList.size() < limit - 1) {  
11.                String match = input.subSequence(index,   
12. m.start()).toString();  
13.                matchList.add(match);  
14.                index = m.end();  
15.            } elseif(matchList.size() == limit - 1) { // last one 
16.                String match = input.subSequence(index,  
17.                                                   
18. input.length()).toString();  
19.                matchList.add(match);  
20.                index = m.end();  
21.            }  
22.        }  
23.        // If no match was found, return this 
24.        if(index == 0)  
25.            returnnewString[] {input.toString()};  
26.        // Add remaining segment 
27.        if(!matchLimited || matchList.size() < limit)  
28.            matchList.add(input.subSequence(index,   
29. input.length()).toString());  
30.        // Construct result 
31.        intresultSize = matchList.size();  
32.        if(limit == 0)  
33.            while(resultSize > 0&&   
34. matchList.get(resultSize-1).equals(""))  
35.                resultSize--;  
36.        String[] result = newString[resultSize];  
37.        returnmatchList.subList(0, resultSize).toArray(result);  
38.    }  
 
  
 这里的match就是split出来的String小对象,它其实是String大对象subSequence的结果。继续看 String.subSequence的代码:
 

   Java代码 
 
 
 
1. public  
2. CharSequence subSequence(intbeginIndex, intendIndex) {  
3.        returnthis.substring(beginIndex, endIndex);  
4. }  
 
  
 

   Java代码 
 
 
 
1. publicString   
2. substring(intbeginIndex, intendIndex) {  
3.    if(beginIndex < 0) {  
4.        thrownewStringIndexOutOfBoundsException(beginIndex);  
5.    }  
6.    if(endIndex > count) {  
7.        thrownewStringIndexOutOfBoundsException(endIndex);  
8.    }  
9.    if(beginIndex > endIndex) {  
10.        thrownewStringIndexOutOfBoundsException(endIndex - beginIndex);  
11.    }  
12.    return((beginIndex == 0) && (endIndex == count)) ? this:  
13.        newString(offset + beginIndex, endIndex - beginIndex, value);  
14.    }  
 
  
 

   Java代码 
 
 
 
1. // Package  
2. privateconstructor which shares value array forspeed.  
3.    String(intoffset, intcount, charvalue[]) {  
4.    this.value = value;  
5.    this.offset = offset;  
6.    this.count = count;  
7.    }  
 
   为了避免内存拷贝、加快速度,Sun JDK直接复用了原String对象的char[],偏移量和长度来标识不同的字符串内容。也就是说,subString出的来String小对象 仍然会指向原String大对象的char[],split也是同样的情况 。这就解释了,为什么HashMap中String对象的char[]都那么大。
 原因解释

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

 解决方案

  
 

   Java代码 
 
 
 
1.     
2.    publicString(String original) {  
3.    intsize = original.count;  
4.    char[] originalValue = original.value;  
5.    char[] v;  
6.    if(originalValue.length > size) {  
7.        // The array representing the String is bigger than the new 
8.        // String itself.  Perhaps this constructor is being called 
9.        // in order to trim the baggage, so make a copy of the array. 
10.            intoff = original.offset;  
11.            v = Arrays.copyOfRange(originalValue, off, off+size);  
12.    } else{  
13.        // The array representing the String is the same 
14.        // size as the String, so no point in making a copy. 
15.        v = originalValue;  
16.    }  
17.    this.offset = 0;  
18.    this.count = size;  
19.    this.value = v;  
20.    }

  

是否Bug


  


  

一些补充

有个地方我没有说清楚。


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


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