標籤:
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/
Java String.substring記憶體泄露?