歡迎您光臨本站 註冊首頁

提高 Java 代碼性能的各種技巧

←手機掃碼閱讀     admin @ 2015-09-17 , reply:0
Java 6,7,8 中的 String.intern – 字元串池
這篇文章將要討論 Java 6 中是如何實現 String.intern 方法的,以及這個方法在 Java 7 以及 Java 8 中做了哪些調整。

字元串池
字元串池(有名字元串標準化)是通過使用唯一的共享 String 對象來使用相同的值不同的地址表示字元串的過程。你可以使用自己定義的 Map (根據需要使用 weak 引用或者 soft 引用)並使用 map 中的值作為標準值來實現這個目標,或者你也可以使用 JDK 提供的String.intern()。
很多標準禁止在 Java 6 中使用 String.intern() 因為如果頻繁使用池會市區控制,有很大的幾率觸發 OutOfMemoryException。Oracle Java 7 對字元串池做了很多改進,你可以通過以下地址進行了解 http://bugs.sun.com/view_bug.do?bug_id=6962931以及http://bugs.sun.com/view_bug.do?bug_id=6962930

Java 6 中的 String.intern()
在美好的過去所有共享的 String 對象都存儲在 PermGen 中 — 堆中固定大小的部分主要用於存儲載入的類對象和字元串池。除了明確的共享字元串,PermGen 字元串池還包含所有程序中使用過的字元串(這裡要注意是使用過的字元串,如果類或者方法從未載入或者被條用,在其中定義的任何常量都不會被載入)
Java 6 中字元串池的最大問題是它的位置 — PermGen。PermGen 的大小是固定的並且在運行時是無法擴展的。你可以使用 -XX:MaxPermSize=N 配置來調整它的大小。據我了解,對於不同的平台默認的 PermGen 大小在 32M 到 96M 之間。你可以擴展它的大小,不過大小使用都是固定的。這個限制需要你在使用 String.intern 時需要非常小心 — 你最好不要使用這個方法 intern 任何無法控制的用戶輸入。這是為什麼在 JAVA6 中大部分使用手動管理 Map 來實現字元串池

Java 7 中的 String.intern()
Java 7 中 Oracle 的工程師對字元串池的邏輯做了很大的改變 — 字元串池的位置被調整到 heap 中了。這意味著你再也不會被固定的內存空間限制了。所有的字元串都保存在堆(heap)中同其他普通對象一樣,這使得你在調優應用時僅需要調整堆大小。這 個改動使得我們有足夠的理由讓我們重新考慮在 Java 7 中使用 String.intern()。

字元串池中的數據會被垃圾收集
沒錯,在 JVM 字元串池中的所有字元串會被垃圾收集,如果這些值在應用中沒有任何引用。這是用於所有版本的 Java,這意味著如果 interned 的字元串在作用域外並且沒有任何引用 — 它將會從 JVM 的字元串池中被垃圾收集掉。
因為被重新定位到堆中以及會被垃圾收集,JVM 的字元串池看上去是存放字元串的合適位置,是嗎?理論上是 — 違背使用的字元串會從池中收集掉,當外部輸入一個字元傳且池中存在時可以節省內存。看起來是一個完美的節省內存的策略?在你回答這個之前,可以肯定的是你 需要知道字元串池是如何實現的。

在 Java 6,7,8 中 JVM 字元串池的實現
字元串池是使用一個擁有固定容量的 HashMap 每個元素包含具有相同 hash 值的字元串列表。一些實現的細節可以從 Java bug 報告中獲得 http://bugs.sun.com/view_bug.do?bug_id=6962930
默認的池大小是 1009 (出現在上面提及的 bug 報告的源碼中,在 Java7u40 中增加了)。在 JAVA 6 早期版本中是一個常量,在隨後的 java6u30 至 java6u41 中調整為可配置的。而在java 7中一開始就是可以配置的(至少在java7u02中是可以配置的)。你需要指定參數 -XX:StringTableSize=N, N 是字元串池 Map 的大小。確保它是為性能調優而預先準備的大小。
在 Java 6 中這個參數沒有太多幫助,因為你仍任被限制在固定的 PermGen 內存大小中。後續的討論將直接忽略 Java 6

Java 7 (直至 Java7u40)
在 Java7 中,換句話說,你被限制在一個更大的堆內存中。這意味著你可以預先設置好 String 池的大小(這個值取決於你的應用程序需求)。通常說來,一旦程序開始內存消耗,內存都是成百兆的增長,在這種情況下,給一個擁有 100 萬字元串對象的字元串池分配 8-16M 的內存看起來是比較適合的(不要使用1,000,000 作為 -XX:StringTaleSize 的值 – 它不是質數;使用1,000,003代替)
你可能期待關於 String 在 Map 中的分配 — 可以閱讀我之前關於 HashCode 方法調優的經驗。
你必須設置一個更大的 -XX:StringTalbeSize 值(相比較默認的 1009 ),如果你希望更多的使用 String.intern() — 否則這個方法將很快遞減到 0 (池大小)。
我沒有注意到在 intern 小於 100 字元的字元串時的依賴情況(我認為在一個包含 50 個重複字元的字元串與現實數據並不相似,因此 100 個字元看上去是一個很好的測試限制)
下面是默認池大小的應用程序日誌:第一列是已經 intern 的字元串數量,第二列 intern 10,000 個字元串所有的時間(秒)
0; time = 0.0 sec
50000; time = 0.03 sec
100000; time = 0.073 sec
150000; time = 0.13 sec
200000; time = 0.196 sec
250000; time = 0.279 sec
300000; time = 0.376 sec
350000; time = 0.471 sec
400000; time = 0.574 sec
450000; time = 0.666 sec
500000; time = 0.755 sec
550000; time = 0.854 sec
600000; time = 0.916 sec
650000; time = 1.006 sec
700000; time = 1.095 sec
750000; time = 1.273 sec
800000; time = 1.248 sec
850000; time = 1.446 sec
900000; time = 1.585 sec
950000; time = 1.635 sec
1000000; time = 1.913 sec
測試是在 Core i5-3317U@1.7Ghz CPU 設備上進行的。你可以看到,它成線性增長,並且在 JVM 字元串池包含一百萬個字元串時,我仍然可以近似每秒 intern 5000 個字元串,這對於在內存中處理大量數據的應用程序來說太慢了。
現在,調整 -XX:StringTableSize=100003 參數來重新運行測試:
50000; time = 0.017 sec
100000; time = 0.009 sec
150000; time = 0.01 sec
200000; time = 0.009 sec
250000; time = 0.007 sec
300000; time = 0.008 sec
350000; time = 0.009 sec
400000; time = 0.009 sec
450000; time = 0.01 sec
500000; time = 0.013 sec
550000; time = 0.011 sec
600000; time = 0.012 sec
650000; time = 0.015 sec
700000; time = 0.015 sec
750000; time = 0.01 sec
800000; time = 0.01 sec
850000; time = 0.011 sec
900000; time = 0.011 sec
950000; time = 0.012 sec
1000000; time = 0.012 sec
可以看到,這時插入字元串的時間近似於常量(在 Map 的字元串列表中平均字元串個數不超過 10 個),下面是相同設置的結果,不過這次我們將向池中插入 1000 萬個字元串(這意味著 Map 中的字元串列表平均包含 100 個字元串)
2000000; time = 0.024 sec
3000000; time = 0.028 sec
4000000; time = 0.053 sec
5000000; time = 0.051 sec
6000000; time = 0.034 sec
7000000; time = 0.041 sec
8000000; time = 0.089 sec
9000000; time = 0.111 sec
10000000; time = 0.123 sec
現在讓我們將吃的大小增加到 100 萬(精確的說是 1,000,003)
1000000; time = 0.005 sec
2000000; time = 0.005 sec
3000000; time = 0.005 sec
4000000; time = 0.004 sec
5000000; time = 0.004 sec
6000000; time = 0.009 sec
7000000; time = 0.01 sec
8000000; time = 0.009 sec
9000000; time = 0.009 sec
10000000; time = 0.009 sec
如你所看到的,時間非常平均,並且與 「0 到 100萬」 的表沒有太大差別。甚至在池大小足夠大的情況下,我的筆記本也能每秒添加1,000,000個字元對象。

我們還需要手工管理字元串池嗎?
現在我們需要對比 JVM 字元串池和 WeakHashMap<String, WeakReference> 它可以用來模擬 JVM 字元串池。下面的方法用來替換 String.intern:
private static final WeakHashMap<String, WeakReference<String>> s_manualCache = 
    new WeakHashMap<String, WeakReference<String>>( 100000 );

private static String manualIntern( final String str )
{
    final WeakReference<String> cached = s_manualCache.get( str );
    if ( cached != null )
    {
        final String value = cached.get();
        if ( value != null )
            return value;
    }
    s_manualCache.put( str, new WeakReference<String>( str ) );
    return str;
}
下面針對手工池的相同測試:
0; manual time = 0.001 sec
50000; manual time = 0.03 sec
100000; manual time = 0.034 sec
150000; manual time = 0.008 sec
200000; manual time = 0.019 sec
250000; manual time = 0.011 sec
300000; manual time = 0.011 sec
350000; manual time = 0.008 sec
400000; manual time = 0.027 sec
450000; manual time = 0.008 sec
500000; manual time = 0.009 sec
550000; manual time = 0.008 sec
600000; manual time = 0.008 sec
650000; manual time = 0.008 sec
700000; manual time = 0.008 sec
750000; manual time = 0.011 sec
800000; manual time = 0.007 sec
850000; manual time = 0.008 sec
900000; manual time = 0.008 sec
950000; manual time = 0.008 sec
1000000; manual time = 0.008 sec
當 JVM 有足夠內存時,手工編寫的池提供了良好的性能。不過不幸的是,我的測試(保留String.valueOf(0 < N < 1,000,000,000))保留非常短的字元串,在使用 -Xmx1280M 參數時它允許我保留月為 2.5M 的這類字元串。JVM 字元串池 (size=1,000,003)從另一方面講在 JVM 內存足夠時提供了相同的性能特性,知道 JVM 字元串池包含 12.72M 的字元串並消耗掉所有內存(5倍多)。我認為,這非常值得你在你的應用中去掉所有手工字元串池。

在 Java 7u40+ 以及 Java 8 中的 String.intern()
Java7u40 版本擴展了字元串池的大小(這是組要的性能更新)到 60013.這個值允許你在池中包含大約 30000 個獨立的字元串。通常來說,這對於需要保存的數據來說已經足夠了,你可以通過-XX:+PrintFlagsFinal JVM 參數獲得這個值。
我嘗試在原始發布的 Java 8 中運行相同的測試,Java 8 仍然支持 -XX:StringTableSize 參數來兼容 Java 7 特性。主要的區別在於 Java 8 中默認的池大小增加到 60013:
50000; time = 0.019 sec
100000; time = 0.009 sec
150000; time = 0.009 sec
200000; time = 0.009 sec
250000; time = 0.009 sec
300000; time = 0.009 sec
350000; time = 0.011 sec
400000; time = 0.012 sec
450000; time = 0.01 sec
500000; time = 0.013 sec
550000; time = 0.013 sec
600000; time = 0.014 sec
650000; time = 0.018 sec
700000; time = 0.015 sec
750000; time = 0.029 sec
800000; time = 0.018 sec
850000; time = 0.02 sec
900000; time = 0.017 sec
950000; time = 0.018 sec
1000000; time = 0.021 sec

測試代碼
這篇文章的測試代碼很簡單,一個方法中循環創建並保留新字元串。你可以測量它保留 10000 個字元串所需要的時間。最好配合 -verbose:gc JVM 參數來運行這個測試,這樣可以查看垃圾收集是何時以及如何發生的。另外最好使用 -Xmx 參數來執行堆的最大值。
這裡有兩個測試:testStringPoolGarbageCollection 將顯示 JVM 字元串池被垃圾收集 — 檢查垃圾收集日誌消息。在 Java 6 的默認 PermGen 大小配置上,這個測試會失敗,因此最好增加這個值,或者更新測試方法,或者使用 Java 7.
第二個測試顯示內存中保留了多少字元串。在 Java 6 中執行需要兩個不同的內存配置 比如: -Xmx128M 以及 -Xmx1280M (10 倍以上)。你可能發現這個值不會影響放入池中字元串的數量。另一方面,在 Java 7 中你能夠在堆中填滿你的字元串。
/**
 - Testing String.intern.
 *
 - Run this class at least with -verbose:gc JVM parameter.
 */
public class InternTest {
    public static void main( String[] args ) {
        testStringPoolGarbageCollection();
        testLongLoop();
    }

    /**
     - Use this method to see where interned strings are stored
     - and how many of them can you fit for the given heap size.
     */
    private static void testLongLoop()
    {
        test( 1000 * 1000 * 1000 );
        //uncomment the following line to see the hand-written cache performance
        //testManual( 1000 * 1000 * 1000 );
    }

    /**
     - Use this method to check that not used interned strings are garbage collected.
     */
    private static void testStringPoolGarbageCollection()
    {
        //first method call - use it as a reference
        test( 1000 * 1000 );
        //we are going to clean the cache here.
        System.gc();
        //check the memory consumption and how long does it take to intern strings
        //in the second method call.
        test( 1000 * 1000 );
    }

    private static void test( final int cnt )
    {
        final List<String> lst = new ArrayList<String>( 100 );
        long start = System.currentTimeMillis();
        for ( int i = 0; i < cnt; ++)
        {
            final String str = "Very long test string, which tells you about something " +
            "very-very important, definitely deserving to be interned #" + i;
//uncomment the following line to test dependency from string length
//            final String str = Integer.toString( i );
            lst.add( str.intern() );
            if ( i % 10000 == 0 )
            {
                System.out.println( i + "; time = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
                start = System.currentTimeMillis();
            }
        }
        System.out.println( "Total length = " + lst.size() );
    }

    private static final WeakHashMap<String, WeakReference<String>> s_manualCache =
        new WeakHashMap<String, WeakReference<String>>( 100000 );

    private static String manualIntern( final String str )
    {
        final WeakReference<String> cached = s_manualCache.get( str );
        if ( cached != null )
        {
            final String value = cached.get();
            if ( value != null )
                return value;
        }
        s_manualCache.put( str, new WeakReference<String>( str ) );
        return str;
    }

    private static void testManual( final int cnt )
    {
        final List<String> lst = new ArrayList<String>( 100 );
        long start = System.currentTimeMillis();
        for ( int i = 0; i < cnt; ++)
        {
            final String str = "Very long test string, which tells you about something " +
                "very-very important, definitely deserving to be interned #" + i;
            lst.add( manualIntern( str ) );
            if ( i % 10000 == 0 )
            {
                System.out.println( i + "; manual time = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
                start = System.currentTimeMillis();
            }
        }
        System.out.println( "Total length = " + lst.size() );
    }
}

總結
  • 由於 Java 6 中使用固定的內存大小(PermGen)因此不要使用 String.intern() 方法
  • Java7 和 8 在堆內存中實現字元串池。這以為這字元串池的內存限制等於應用程序的內存限制。
  • 在 Java 7 和 8 中使用 -XX:StringTableSize 來設置字元串池 Map 的大小。它是固定的,因為它使用 HashMap 實現。近似於你應用單獨的字元串個數(你希望保留的)並且設置池的大小為最接近的質數並乘以 2 (減少碰撞的可能性)。它是的 String.intern 可以使用相同(固定)的時間並且在每次插入時消耗更小的內存(同樣的任務,使用java WeakHashMap將消耗4-5倍的內存)。
  • 在 Java 6 和 7(Java7u40以前) 中 -XX:StringTableSize 參數的值是 1009。Java7u40 以後這個值調整為 60013 (Java 8 中使用相同的值)
  • 如果你不確定字元串池的用量,參考:-XX:+PrintStringTableStatistics JVM 參數,當你的應用掛掉時它告訴你字元串池的使用量信息。
  • [*]

[admin ] 提高 Java 代碼性能的各種技巧已經有475次圍觀

http://coctec.com/docs/java/show-post-204816.html