Java中由substring方法引發的記憶體洩漏
在Java中我們無須關心記憶體的釋放,JVM提供了記憶體管理機制,有垃圾回收器幫助回收不需要的物件。但實際中一些不當的使用仍然會導致一系列的記憶體問題,常見的就是記憶體洩漏和記憶體溢位
記憶體溢位(out of memory ):通俗的說就是記憶體不夠用了,比如在一個無限迴圈中不斷建立一個大的物件,很快就會引發記憶體溢位。
記憶體洩漏(leak of memory):是指為一個物件分配記憶體之後,在物件已經不在使用時未及時的釋放,導致一直佔據記憶體單元,使實際可用記憶體減少,就好像記憶體洩漏了一樣。
由substring方法引發的記憶體洩漏
substring(int beginIndex, int endndex )是String類的一個方法,但是這個方法在JDK6和JDK7中的實現是完全不同的(雖然它們都達到了同樣的效果)。瞭解它們實現細節上的差異,能夠更好的幫助你使用它們,因為在JDK1.6中不當使用substring會導致嚴重的記憶體洩漏問題。
1、substring的作用
substring(int beginIndex, int endIndex)方法返回一個子字串,從父字串的beginIndex開始,結束於endindex-1。父字串的下標從0開始,子字串包含beginIndex而不包含endIndex。
String x= "abcdef";
x= str.substring(1,3);
System.out.println(x);
上述程式的輸出是“bc”
2、實現原理
String類是不可變變,當上述第二句中x被重新賦值的時候,它會指向一個新的字串物件,就像下面的這幅圖所示:
然而,這幅圖並沒有準確說明的或者代表堆中發生的實際情況,當substring被呼叫的時候真正發生的才是這兩者的差別。
JDK6中的substring實現
String物件被當作一個char陣列來儲存,在String類中有3個域:char[] value、int offset、int count,分別用來儲存真實的字元陣列,陣列的起始位置,String的字元數。由這3個變數就可以決定一個字串。當substring方法被呼叫的時候,它會建立一個新的字串,但是上述的char陣列value仍然會使用原來父陣列的那個value。父陣列和子陣列的唯一差別就是count和offset的值不一樣,下面這張圖可以很形象的說明上述過程。
看一下JDK6中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); //使用的是和父字元串同一個char陣列value }
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
由此引發的記憶體洩漏洩漏情況:
String str = "abcdefghijklmnopqrst";
String sub = str.substring(1, 3);
str = null;
這段簡單的程式有兩個字串變數str、sub。sub字串是由父字串str擷取得到的,假如上述這段程式在JDK1.6中執行,我們知道陣列的記憶體空間分配是在堆上進行的,那麼sub和str的內部char陣列value是公用了同一個,也就是上述有字元a~字元t組成的char陣列,str和sub唯一的差別就是在陣列中其實beginIndex和字元長度count的不同。在第三句,我們使str引用為空,本意是釋放str佔用的空間,但是這個時候,GC是無法回收這個大的char陣列的,因為還在被sub字串內部引用著,雖然sub只擷取這個大陣列的一小部分。當str是一個非常大字串的時候,這種浪費是非常明顯的,甚至會帶來效能問題,解決這個問題可以是通過以下的方法:
String str = "abcdefghijklmnopqrst";
String sub = str.substring(1, 3) + "";
str = null;
利用的就是字串的拼接技術,它會建立一個新的字串,這個新的字串會使用一個新的內部char陣列儲存自己實際需要的字元,這樣父陣列的char陣列就不會被其他引用,令str=null,在下一次GC回收的時候會回收整個str佔用的空間。但是這樣書寫很明顯是不好看的,所以在JDK7中,substring 被重新實現了。
JDK7中的substring實現
在JDK7中改進了substring的實現,它實際是為擷取的子字串在堆中建立了一個新的char陣列用於儲存子字串的字元。下面的這張圖說明了JDK7中substring的實現過程:
檢視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);
}
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]; //是建立了一個新的char陣列
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
可以發現是去為子字串建立了一個新的char陣列去儲存子字串中的字元。這樣子字串和父字串也就沒有什麼必然的聯絡了,當父字串的引用失效的時候,GC就會適時的回收父字串佔用的記憶體空間。