对于substring内存泄漏的分析

什么是内存泄漏?所谓内存泄漏,简单地说,就是由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,它并不是说物理内存消失了,而是指由于不再使用的对象占据内存不被释放,而导致可用内存不断减小,最终有可能导致内存溢出。
由于垃圾回收器的出现,与传统的C/C++相比,Java 已经把内存泄漏的概率大大降低了,
所有不再使用的对象会由系统自动收集,但这并不意味着已经没有内存泄漏的可能。内存泄漏实际上更是一个应用问题,这里以String.substring()方法为例,说明这种内存泄漏的问题。
在 JDK 1.6 中,java.lang.String 主要由3 部分组成:代表字符数组的value、偏移量offset和长度count

String 对象
value char 数组
offset 偏移
count 长度
这个结构为内存泄漏埋下了伏笔,字符串的实际内容由value、offset 和count 三者共同决定,而非value 一项。试想,如果字符串value 数组包含100 个字符,而count 长度只有1 个字节,那么这个String 实际上只有1 个字符,却占据了至少100 个字节,那剩余的99 个就属于泄漏的部分,它们不会被使用,不会被释放,却长期占用内存,直到字符串本身被回收。可以看到,str 的count 为1,而它的实际取值为字符串“0”,但是在value 的部分,却包含了上万个字节,在这个极端情况中,原本只应该占用1 个字节的String,却占用了上万个字节,因此,可以判定为内存泄漏。
不幸的是,这种情况在JDK 1.6 中非常容易出现。使用String.substring()方法就可以很容易地构造这么一个字符串。下面简单解读一下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。
该构造函数的实现如下:
// 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 并没有从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,而是将实际需要的部分做了复制,该问题也得到了完全的修复。
在虚拟机中,有一块称为常量池的区间专门用于存放字符串常量。在JDK 1.6 之前,这块区间属于永久区的一部分,但是在JDK 1.7 以后,它就被移到了堆中进行管理。

可以写一个测试的代码加以验证:

public class Huge {
    private String str = new String(new char[100000]);

    public String getSub(int begin, int end) {
        return str.substring(begin, end);
    }
}
public class SubStringProblem {

    public static void main(String []args){
        List<String> testString=new ArrayList<String>();
        for(int i=0;i<10000;i++){
            Huge hu=new Huge();
            testString.add(hu.getSub(0, 5));
            try {
                Thread.currentThread();
                Thread.sleep(100);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

运行这个代码,使用jps获得这个进程的进程号,再使用jmap -dump:format=b,file=filename pid 来获得内存dump文件,
运行IBM的HeapAnalyzer 来对文件进行分析,命令如下:java -Xmx1000m -jar F:/HeapAnalyzer/ha39.jar 。

可以看到,所有substring所占的内存依然是总的原来字符串的内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值