如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析
- 把Java应用程序使用的heap dump下来
- 使用Java heap分析工具,找出内存占用超出预期(一般是因为数量太多)的嫌疑对象
- 必要时,需要分析嫌疑对象和其他对象的引用关系。
- 查看程序的源代码,找出嫌疑对象数量过多的原因。
dump heap
- jmap
-dump:format=b,file=heap.bin <pid> - jmap -dump:format=b,file=xxx.bin pid pid是java程序pid
analyze heap
顺藤摸瓜
查看代码
- public
- String[]
split(String regex, int limit) { -
return Pattern.compile(regex).split(this, limit); - }
可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:
- 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); -
}
这里的match就是split出来的String小对象,它其实是String大对象subSequence的结果。继续看 String.subSequence的代码:
- public
- CharSequence
subSequence(int beginIndex, int endIndex) { -
return this.substring(beginIndex, endIndex); - }
- public
String - substring(int
beginIndex, int endIndex) { -
if (beginIndex < 0) { -
throw new StringIndexOutOfBoundsEx ception(beginIndex); -
} -
if (endIndex > count) { -
throw new StringIndexOutOfBoundsEx ception(endIndex); -
} -
if (beginIndex > endIndex) { -
throw new StringIndexOutOfBoundsEx ception(endIndex - beginIndex); -
} -
return ((beginIndex == 0) && (endIndex == count)) ? this : -
new String(offset + beginIndex, endIndex - beginIndex, value); -
}
- //
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; -
}
原因解释
- 程序从每个请求中得到一个String大对象,该对象内部char[]的长度达数百K。
- 程序对String大对象做split,将split得到的String小对象放到HashMap中,用作缓存。
- Sun JDK6对String.split方法做了优化,split出来的Stirng对象直接使用原String对象的char[]
- HashMap中的每个String对象其实都指向了一个巨大的char[]
- HashMap的上限是万级的,因此被缓存的Sting对象的总大小=万*百K=G级。
- G级的内存被缓存占用了,大量的内存被浪费,造成内存泄露的迹象。
解决方案
-
-
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; -
}
是否Bug
一些补充
有个地方我没有说清楚。
我的程序是一个Web程序,每次接受请求,就会创建一个大的String对象,然后对该String对象进行split,最后split之后的 String对象放到全局缓存中。如果接收了5W个请求,那么就会有5W个大String对象。这5W个大String对象都被存储在全局缓存中,因此会 造成内存泄漏。我原以为缓存的是5W个小String,结果都是大String。
“抛出异常的爱”同学,在回帖(第7页)中建议用"java.io.StreamTokenizer"来解决本文的问题。确实是终极解决方案,比我上面提到的“new String()”,要好很多很多