读书笔记之Java程序性能优化

最近在“Java程序性能优化”一书上看到“subString内存溢出优化”,一开始以为这是Java的Bug,看完才明白Java是采用空间换时间的策略,来提升生成字符串的速度,但因为牺牲了空间所以有可能出现内存溢出。

首先说明一下:因为此问题是在Java6会出现,而Java7采用其他方法避免了。所以我演示使用JDK分别是jdk1.6.0_45和jdk1.7.0_40。

问题重现

因为Java官网有说明,所以使用他们代码重现这个Bug。

Bug地址:http://bugs.java.com/view_bug.do?bug_id=4513622

public class TestGC {
    private String largeString = new String(new byte[100000]);

    String getString() {
        return this.largeString.substring(0,2);
    }

    public static void main(String[] args) {
        java.util.ArrayList list = new java.util.ArrayList();
        for (int i = 0; i < 1000000; i++) {
            TestGC gc = new TestGC();
            list.add(gc.getString());
        }
    }
}

上面的代码在Java 6 (Java 7不会抛出异常)运行一下就会报java.lang.OutOfMemoryError: Java heap space的异常,这说明没有足够的堆内存供我们创建对象,JVM选择了抛出异常操作。

于是有人会说,是因为你每个循环中创建了一个TestGC对象,虽然我们加入ArrayList只是两个字符的字符串,但是这个对象中又存储largeString这么大的对象,这样必然会造成OOM的。

然而,其实你说的不对。比如我们看一下这样的代码,我们只修改getString方法。

public class TestGC {
    private String largeString = new String(new byte[100000]);

    String getString() {
        //return this.largeString.substring(0,2);
      return new String("ab");
    }

    public static void main(String[] args) {
        java.util.ArrayList list = new java.util.ArrayList();
        for (int i = 0; i < 1000000; i++) {
            TestGC gc = new TestGC();
            list.add(gc.getString());
        }

    }
}

执行上面的方法,并不会导致OOM异常,因为我们持有的时1000000个ab字符串对象,而TestGC对象(包括其中的largeString)会在java的垃圾回收中释放掉。所以这里不会存在内存溢出。

那么究竟是什么导致的内存泄露呢?要研究这个问题,我们需要看一下方法的实现,即可。

源码分析

在String类中存在这样三个属性

value 字符数组,存储字符串实际的内容

offset 该字符串在字符数组value中的起始位置

count 字符串包含的字符的长度

Java 6中substring的实现有两个:

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)

查阅源代码可知,第一个substring实质是调用第二个substring的,如下:

public String substring(int beginIndex) {
      return substring(beginIndex, count);
}

第二个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);
}

方法最后,返回一个新建的String对象。查看该Stirng 的构造函数:

//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数组被复制到新的子字符串中。设想,如果原字符串很大,截取的字符串长度缺很短,那么截取的字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了运算速度却浪费了大量的内存空间。

如何解决

对于之前比较不常见的1G字符串只截取2个字符的情况可以使用下面的代码,这样的话,就不会持有1G字符串的内容数组引用了。

String littleString = new String(largeString.substring(0,2));

下面的这个构造方法,在源字符串内容数组长度大于字符串长度时,进行数组复制,新的字符串会创建一个只包含源字符串内容的字符数组。

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;
}

Java 7 实现

在Java 7 中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);
}

substring方法中调用的构造方法,进行内容字符数组复制。

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);
}

参考资料

Java程序性能优化一书

Java对此Bug的说明:http://bugs.java.com/view_bug.do?bug_id=4513622

http://droidyue.com/blog/2014/12/14/substring-memory-issue-in-java/

http://mouselearnjava.iteye.com/blog/1929037

http://www.cnblogs.com/techyc/p/3324021.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值