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

来源:互联网 发布:pta编程 编辑:程序博客网 时间:2024/05/21 07:05

最近在“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

0 0
原创粉丝点击