1. 程式人生 > >深入JVM-有關String的記憶體洩漏

深入JVM-有關String的記憶體洩漏

什麼是記憶體洩漏?所謂記憶體洩漏,就是由於疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況,他並不是說實體記憶體消失了,而是指由於不再使用的物件佔據了記憶體不被釋放,而導致可用記憶體不斷減小,最終有可能導致記憶體溢位。

由於垃圾回收器的出現,與傳統的C/C++相比,Java已經把記憶體洩漏的概率大大降低了,所以不再使用的物件會由系統自動收集,但這並不意味著已經沒有記憶體洩漏的可能。記憶體洩漏實際上更是一個應用問題,這裡以String.substring()方法為例,說明這種記憶體洩漏的問題。

在JDK 1.6中,java.lang.String主要由3部分組成:代表字元資料的value、偏移量offset和長度count。

這個結構為記憶體洩漏埋下了伏筆,字串的實際內容由value、offset和count三者共同決定,而非value一項。試想,如果字串value陣列包含了100個字元,而count長度只有1個位元組,那麼這個string實際上只有1個字元,卻佔據了至少100個位元組,那剩餘的99個就屬於洩漏的部分,他們不會被使用,不會被釋放,卻長期佔用記憶體,直到字串本身被回收。

不幸的是,這種情況在JDK 1.6中非常容易出現。下面簡單解讀一下JDK 1.6中String.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); }

可以看到,在substring()的視線中,最終是使用了String的建構函式,生成了一個新的String。該建構函式的實現如下:

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

該建構函式並非公有建構函式。這點應該萬幸,因為正是這個建構函式引起了記憶體洩漏問題。新生成的String並沒有從value中獲取自己需要的那部分,而是簡單的使用了相同的value引用,只是修改了offset和count,以此來確定新的String物件的值。當原始字串沒有被回收時,這種情況是沒有問題的,並且通過公用value,還可以節省一部分記憶體,但是一旦原始字串被回收,value中多餘的部分就造成了空間浪費。

綜上所述,如果使用了String.substring()將一個大字串切割為小字串,當大字串被回收時,小字串的存在就會引起記憶體洩漏。

所幸,這個問題已經引起了官方的重視,在JDK 1.7中,對String的實現有了大幅度的調整。在新版本的String中,去掉了offset和count兩項,而String的實質性內容僅僅由value決定,而value陣列本身也就代表了這個String實際的取值。下面簡單的對比String.length()方法來說明這個問題,程式碼如下:

//JDK 1.7 實現
public int length() {
    return value.length;
}

//JDK 1.6 實現
public int length() {
    return count;
}

可以看到,在JDK 1.6中,String長度和value無關。基於這種改進的實現,substring()方法的記憶體洩漏問題也得以解決,如下程式碼所示,展示了JDK 1.7 中的String.substring()實現。

public String substring(int beginIndex, int endIndex) {
    //省略部分無關內容
    int subLen = endIndex - beginIndex;
    //省略部分無關內容
    return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen);
}

public String(char value[], int offset, int 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);
}

從上述程式碼可以看到,在新版本的substring中,不再複用原String的value,而是將實際需要的部分做了複製,該問題也得到了完全的修復。