2010年1月10日 星期日

[Java] 小心 substring 的 memory leak

這個內容並不是我自己發現的,剛好在 PTT 的 Java 版中看到某位高手所說得!其實,我是今天才知道有這回事!
簡單的來說,假設我們有一段程式碼:

public class SubstringMemoryLeak {
    
    private String str = new String(new byte[10000]);

    public String substring() {
        return this.str.substring(0, 2);
    }
    /**
     * @param args
     */
    public static void main(String[] args) {
        List<String> substringList = new ArrayList<String>(10000);
        for (int i=0, n=10000; i<n; i++) {
            substringList.add(new SubstringMemoryLeak().substring());
        }
    }

}

如果執行上面的程式碼,你就會出現 Exception:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.StringCoding$StringDecoder.decode(StringCoding.java:133)
    at java.lang.StringCoding.decode(StringCoding.java:173)
    at java.lang.StringCoding.decode(StringCoding.java:185)
    at java.lang.String.(String.java:570)
    at java.lang.String.
(String.java:593)
    at silver8250.tools.SubstringMemoryLeak.
(SubstringMemoryLeak.java:8)
    at silver8250.tools.SubstringMemoryLeak.main(SubstringMemoryLeak.java:19)


但是如果將 substring() method 修改成:


public String substring() {
    return new String(this.str.substring(0, 2));
}
神奇的事情發生了!程式竟然可以如願的執行!問題出現在哪?上面的程式碼最主要是建立一堆很佔記憶體的 String 物件,然後取其中的一小段!重點就在於 String 的 substring() method 的實做方式。
首先,String 物件在記憶體中會以 char array 方式呈現,當我們每次建立一個 String 時,記憶體就會長出一塊 char array 來存放。String class 有一個 non-public constructor:

String (int offset, int count, char[] value)

當我們使用 substring 時,實際是用這個 constructor 來完成,也就是原先建立很佔記憶體的 String 物件並不會縮小,而是保持原來的 char array 所佔的大小。所以 Java 的 GC 就無法對此 char array 進行回收的動作。所以使用 substring() method 對字串內容來說,我們看到的是部份的,但是在記憶體中卻是佔有原先的大小!

後來,我們改用 new String() constructor 來包裝 substring 的內容,這就會讓 Java 重新建一個 char array 來放置 substring 的內容,相對的,所佔得記憶體就小了,而且原先較大的 char array 就沒有任何 reference ,所以 GC 就可以直接回收了!

老實說,這種情況可能不太常見,至少對我來說啦!我對於 String 的建立都會盡量改用 StringBuffer 物件,這樣不僅可以提供較好得效能,對於記憶體的利用也比較不會有問題!

與大家分享之~