深入JVM-有关String的内存泄漏
什么是内存泄漏?所谓内存泄漏,就是由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,他并不是说物理内存消失了,而是指由于不再使用的对象占据了内存不被释放,而导致可用内存不断减小,最终有可能导致内存溢出。
由于垃圾回收器的出现,与传统的C/C++相比,Java已经把内存泄漏的概率大大降低了,所以不再使用的对象会由系统自动收集,但这并不意味着已经没有内存泄漏的可能。内存泄漏实际上更是一个应用问题,这里以String.substring()方法为例,说明这种内存泄漏的问题。
在JDK 1.6中,java.lang.String主要由3部分组成:代表字符数据的value、偏移量offset和长度count。
这个结构为内存泄漏埋下了伏笔,字符串的实际内容由value、offset和count三者共同决定,而非value一项。试想,如果字符串value数组包含了100个字符,而count长度只有1个字节,那么这个string实际上只有1个字符,却占据了至少100个字节,那剩余的99个就属于泄漏的部分,他们不会被使用,不会被释放,却长期占用内存,直到字符串本身被回收。
不幸的是,这种情况在JDK 1.6中非常容易出现。下面简单解读一下JDK 1.6中String.substring()的实现。
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);
}
可以看到,在substring()的视线中,最终是使用了String的构造函数,生成了一个新的String。该构造函数的实现如下:
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
该构造函数并非公有构造函数。这点应该万幸,因为正是这个构造函数引起了内存泄漏问题。新生成的String并没有从value中获取自己需要的那部分,而是简单的使用了相同的value引用,只是修改了offset和count,以此来确定新的String对象的值。当原始字符串没有被回收时,这种情况是没有问题的,并且通过公用value,还可以节省一部分内存,但是一旦原始字符串被回收,value中多余的部分就造成了空间浪费。
综上所述,如果使用了String.substring()将一个大字符串切割为小字符串,当大字符串被回收时,小字符串的存在就会引起内存泄漏。
所幸,这个问题已经引起了官方的重视,在JDK 1.7中,对String的实现有了大幅度的调整。在新版本的String中,去掉了offset和count两项,而String的实质性内容仅仅由value决定,而value数组本身也就代表了这个String实际的取值。下面简单的对比String.length()方法来说明这个问题,代码如下:
//JDK 1.7 实现
public int length() {
return value.length;
}
//JDK 1.6 实现
public int length() {
return count;
}
可以看到,在JDK 1.6中,String长度和value无关。基于这种改进的实现,substring()方法的内存泄漏问题也得以解决,如下代码所示,展示了JDK 1.7 中的String.substring()实现。
public String substring(int beginIndex, int endIndex) {
//省略部分无关内容
int subLen = endIndex - beginIndex;
//省略部分无关内容
return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
//省略部分无关内容
//Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
从上述代码可以看到,在新版本的substring中,不再复用原String的value,而是将实际需要的部分做了复制,该问题也得到了完全的修复。