1. 程式人生 > >Java中由substring方法引發的記憶體洩漏

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就會適時的回收父字串佔用的記憶體空間。