目錄
Java 6,7,8 中的String.intern – 字串池
字串池
Java 7 中的String.intern()
字串池中的資料會被垃圾收集
在Java 6,7,8 中JVM 字串池的實作
Java 7 (直至Java7u40)
我們還需要手動管理字串池嗎?
在Java 7u40+ 以及Java 8 中的String.intern()
測試程式碼
总结
首頁 Java java教程 提升 Java 程式碼效能的各種技巧分享

提升 Java 程式碼效能的各種技巧分享

Apr 17, 2017 am 11:37 AM

[導讀] Java 6,7,8 中的String intern – 字串池這篇文章將要討論Java 6 中是如何實作String intern 方法的,以及這個方法在Java 7 以及Java 8 中做了哪些調整。字串池字串池(有名字串標準化)                

在 Java 6 這個參數中沒有太多幫助,因為你仍被限制在固定的 PermGen 記憶體大小中。後續的討論將直接忽略Java 6

你必須設定一個更大的-XX:StringTalbeSize 值(相比較預設的1009 ),如果你希望更多的使用String.intern() — 否則這個方法很快就會遞減到0 (池大小)。

Java 6,7,8 中的String.intern – 字串池

#這篇文章將要討論Java 6 中是如何實作String.intern方法的,以及這個方法在Java 7 以及Java 8 中做了哪些調整。

字串池

字串池(有名字串標準化)是透過使用唯一的共享String 物件來使用相同的值不同的地址表示字串的過程。你可以使用自己定義的<a href="http://www.php.cn/code/8210.html" target="_blank">Map</a><String, String> (根據需要使用weak 引用或soft 引用)並使用map 中的值作為標準值來實現這個目標,或者你也可以使用JDK 提供的String.intern()

很多標準禁止在Java 6 中使用String.intern() 因為如果經常使用池會市區控制,有很大的幾率觸發OutOfMemory<a href="http://www.php.cn/wiki/265.html" target="_blank">#Exception</a>。 Oracle Java 7 對字串池做了許多改進,你可以透過以下位址進行了解bugs.sun.com/view_bug.do?bug_id=6962931以及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 報表中取得 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<String>> 它可以用來模擬 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; ++i )
        {
            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; ++i )
        {
            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 参数,当你的应用挂掉时它告诉你字符串池的使用量信息。

以上是提升 Java 程式碼效能的各種技巧分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java 中的史密斯數 Java 中的史密斯數 Aug 30, 2024 pm 04:28 PM

Java 史密斯數指南。這裡我們討論定義,如何在Java中檢查史密斯號?帶有程式碼實現的範例。

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

Java 中的時間戳至今 Java 中的時間戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的時間戳記到日期指南。這裡我們也結合範例討論了介紹以及如何在java中將時間戳記轉換為日期。

Java程序查找膠囊的體積 Java程序查找膠囊的體積 Feb 07, 2025 am 11:37 AM

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

創造未來:零基礎的 Java 編程 創造未來:零基礎的 Java 編程 Oct 13, 2024 pm 01:32 PM

Java是熱門程式語言,適合初學者和經驗豐富的開發者學習。本教學從基礎概念出發,逐步深入解說進階主題。安裝Java開發工具包後,可透過建立簡單的「Hello,World!」程式來實踐程式設計。理解程式碼後,使用命令提示字元編譯並執行程序,控制台上將輸出「Hello,World!」。學習Java開啟了程式設計之旅,隨著掌握程度加深,可創建更複雜的應用程式。

See all articles