在《Java程序性能优化》3.12小节用一个例子说明在Java 6之前的版本(Java 7中已经解决)使用subString方法可能会带来性能的问题,但是并没有说明为什么会出现性能问题,我从JVM内存模型层面试着分析这个问题。
源代码:
public class Test {
public static void main(String[] args) {
List<String> handler = new ArrayList<String>();
for (int i = 0; i < 1000; i++) {
HugeStr h = new HugeStr(); // Line 1
//ImprovedHugeStr h = new ImprovedHugeStr(); // Line 2
handler.add(h.getSubString(1, 5)); // Line 3
}
}
static class HugeStr {
private String str = new String(new char[100000]);
public String getSubString(int begin, int end) {
return str.substring(begin, end);
}
}
static class ImprovedHugeStr {
private String str = new String(new char[100000]);
public String getSubString(int begin, int end) {
return new String(str.substring(begin, end));
}
}
}
这是HugeStr一次for循环的简易内存分配示意图。
说明:
Line 1这一行实际上做了两个操作,在堆中创建了一个新的HugeStr对象,并将h指向堆中新创建的HugeStr对象;而HugeStr对象中又包含一个str属性,同理,此刻str将指向内存中另外String对象,该String对象包含一个value属性指向一个100000长度的数组。
Line 3这一行关注h.getSubString()这个操作,在Java 7以前,这里会新new一个字符串,并将原字符串的value的引用传递给新的字符串,如说上3.12第三幅图展示的样子,这样新的字符串的value属性则指向了Line 1上那个长度很长的字符串。
当循环结束后,h的引用变成无效引用,那么其他相关联的无效引用如图中红色X所示。但是100000长度的数组则不能被确定为无效引用,因为ArrayList.add()方法执行之后,会将Line 3生成的对象放置在该集合中。
对比ImprovedHugeStr
说明:ImprovedHugeStr与HugeStr唯一的区别在于在getSubstring()方法的时候新new了一个字符串,而正好是这个new操作,Java将会将原来value的数组复制并生成新的数组,也就使得大数组的引用在经过循环之后变成了无效引用,当垃圾回收器需要进行gc的时候会将其回收。上JDK源码。
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;
}