subString()方法的内存泄漏

阅读更多

    在java中我们无须关心内存的释放,JVM提供了内存管理机制,有GC回收不需要的对象。但实际使用中仍然会导致一系列的内存问题,常见的就是内存泄漏和内存溢出。

    内存泄漏(leak of memory): 是指为一个对象分配内存之后,在对象已经不再使用时未及时的释放,导致一直占据内存单元,使实际可用内存减少,就好像内存泄漏了一样。

    内存溢出(out of memory):  通俗的说就是内存不够用了,比如在一个无限循环中不断创建一个大的对象,很快就会引发内存溢出。

 

    由subString方法引发的内存泄漏

    substring(int beginIndex, int endIndex)是String类提供的的一个截取字符串的方法,但是这个方法在JDK6和JDK7中的实现是不同的,了解他们实现的细节上的差异,能够帮助避免在JDK6中不当使用substring导致内存泄漏的问题。

    1. substring的作用

    substring(int beginIndex, int endIndex)方法返回一个子字符串,从父字符串的beginIndex开始,结束语endIndex-1. 父字符串的下标充0开始,子字符串包含beginIndex而不包含endIndex。

 

String x = "abcdef";
x = x.substring(1,3);
System.out.println(x);

    上述程序输出的是“bc”

 

    2. 实现原理

    String类是不可变的,当上述第二句中x被重新赋值的时候,它会指向一个新的字符串对象,如下图所示:

subString()方法的内存泄漏_第1张图片

    然而,这幅图并没有准确说明堆中发生的实际情况,当substring被调用的时候真正发生的才是两者(JDK6和JDK7)的差别。

   

    JDK6中的substring实现

    String对象被当做一个char数组来存储,在String类中有3个域:char[] value、int offset、int count, 分别用来存储真是的字符串数组,数组的起始位置,String的字符数。由这3个变量就可以决定一个字符串。当substring方法被调用的时候,它会创建一个新的字符串,但是上述的char数组value仍然会使用原来父数组的那个value。父数组和子数组的唯一差别就是count和offset的位置不一样,下图形象的说明了这一过程:

subString()方法的内存泄漏_第2张图片

    查看此方法的源代码:

 

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); //使用的是和父字符串同一个char数组value  
}

     在方法最后,返回了一个新建的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内的char数组对象。但在这种通过偏移量来截取字符串的方法中,String原生内容value被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符串长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占用了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了运算的速度,却浪费了大量的内存空间。(以空间换时间策略)

    由此,引发的内存泄漏情况:

String str = new String(new char[100000]);
String sub = str.substring(1,3);
str = null

    这段程序有两个字符串变量str、sub。sub字符串是由父字符串str截取得到的,假如上述这段程序在JDK6中运行,那么sub和str的内部char数组value是公用了同一个,str和sub的差别就在于数组中的起始位置beginIndex和字符串长度count的不同。在第3句,我们使str引用为空,本意是释放str占用的内存空间,但是这个时候GC是无法回收这个大的char数组的,因为还在被sub字符串内部引用着,这种浪费是非常明显的,这就很容易导致内存问题,解决这个问题,可以通过以下方法:

String str = new String(new char[100000]);
String sub = str.substring(1,3) + "";
str = null

    利用的就是字符串的拼接技术,它会创建一个新的字符串,这个新的字符串会使用一个新的内部char数组存储自己实际需要的字符,这样父字符串的char数组就不会被其他引用,令str=null,在下一次GC回收的时候会回收整个str占用的空间。

 

    JDK7中的substring实现

    在JDK7中改进了substring的实现,它实际是为截取的字符串在堆中创建了一个新的插入数组用于保存字符串的字符。下图说明了JDK7中substring的实现过程:

subString()方法的内存泄漏_第3张图片

 

    查看JDK7中String类的substring方法的源码:

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

    最后新建的String的构造方法如下:

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(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);
    }

    Arrays类的copyOfRange方法如下:

    public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        char[] copy = new char[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }

    由此可见,JDK7中substring是为字符串创建了一个新的char数组去存储子字符串中的字符,这样子字符串和父字符串就没什么必然的联系了,当父字符串的引用失效的时候,GC就会适时的回收父字符串占用的内存空间。

    

    PS: 补充说明String对象及其特点

    在Java中,String对象可以认为是char数组的延伸和进一步封装。它主要有3部分组成:char数组、偏移量、String的长度。char数组表示String的内容,它是String对象所表示字符串的超集。String的真实内容还需要由偏移量和长度在这个char数组中进行定位和截取。

    String对象有3个基本特点:

  1. 不变性
  2. 针对常量池的优化
  3. 类的final定义
  • 不变性是指String对象一旦生成,则不能对它进行改变。String的这个特性可以泛化成不变(immutable)模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式主要作用于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
  • 针对常量池的优化是指:当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。
    String str1 = "abc";
    String str2 = "abc";
    String str3 = new String("abc");
    System.out.println(str1 == str2);           //返回true
    System.out.println(str1 == str3);           //返回false
    System.out.println(str1 == str3.intern());           //返回true
     以上代码显示str1 和 str2引用了相同的地址,但是str3重新开辟了一块内存空间,但即便如此,str3在常量池中的位置和str1是一样的,也就是说,虽然str3单独占用了堆空间,但是它所指向的实体和str1完全一样。最后一行使用了intern()方法,该方法返回了String对象在常量池中的应用。

你可能感兴趣的:(subString()方法的内存泄漏)