String可以说是最常用的Java类型之一了,但是最近听说JDK6里面String.substring存在内存泄露的bug,伙惊呆!一起来看看到底是啥情况吧。
这个是可以导致Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 的代码:
public class TestGC { private String largeString = new String(new byte[100000]); String getString() { return this.largeString.substring(0, 2);//在JDK6里会导致out of memory,在JDK7和8不会出现问题// return new String("ab");// return this.largeString.substring(0,2) + "";//JDK6下的解决方法,不会出现out of memory// return new String(this.largeString.substring(0, 2));/JDK6下的解决方法,不会出现out of memory } public static void main(String[] args) { java.util.List<String> list = new java.util.ArrayList<String>(); for (int i = 0; i < 100000; i++) { TestGC gc = new TestGC(); list.add(gc.getString()); } System.out.println("over" + list.size()); } }
但是用JDK8运行,平安无事。注意,之前看的网上文章又说安装了JDK8,只需要在Eclipse里面选Compiler选项为JDK6就可以了,我实 验是不可以的,自己想想String是JDK里面rt.jar的类,就算是编译为JDK6的代码,运行的时候还是用的JDK8的String啊,所以无法 复现bug才是正常的。要复现,只能下载安装JDK6.
有人认为这个会out of memory是因为TestGC对象里面有很大largeString的对象,但是其实在调用getString方法后,TestGC对象完全可以被回收 的,largeString也可以回收,JVM的自动垃圾回收应该不会有bug吧,不然还得了!将getString方法改为直接返回一个String对 象,就可以看出,不会有问题。
现在来看看为什么JDK6里面,substring会导致错误呢。Ctrl+B(IDEA的查看源码快捷键点进去看下),代码如下
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); }
前面几行主要是做范围检查,最主要的是
new String(offset + beginIndex, endIndex - beginIndex, value);
String(int offset, int count, char value[]) {this.value = value;this.offset = offset;this.count = count; }
可以看到JDK6里的substring复用了原来大String的整个value,即String里存放实际char的数组
/** The value is used for character storage. */ private final char value[];
而只是通过修改beginIndex和offset来达到复用value,避免数组copy的麻烦(以及可以提高一点性能),但是问题就是,如果原 String很大,而substring保留的时间比较久,就有可能导致整个很大的value无法回收。JDK6下的修复方法就是,强制生成一个新的 String,避免复用原来String里的value,比如:
return this.largeString.substring(0,2) + "";//JDK6下的解决方法,不会出现out of memory
其实,这恰恰也是JDK8里面的实现方式。上src:
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); }
跟前面区别不大,再来看
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); }
可以看到,最后对value做了数组copy。
其实JDK8的修改也是褒贬不一,也有人认为JDK6里面的实现方法更好,效率更高,只要自己注意就可以避免问题的,这就是仁者见仁智者见智的问题了,只是需要知道,JDK6里String的这个小坑就好。
参考文章
- http://droidyue.com/blog/2014/12/14/substring-memory-issue-in-java/
- http://www.programcreek.com/2013/09/the-substring-method-in-jdk-6-and-jdk-7/