搜尋
首頁Javajava教程提升 Java 程式碼效能的各種技巧分享

[導讀] 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></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 )保留非常短的字串,在使用<code>-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
带你搞懂Java结构化数据处理开源库SPL带你搞懂Java结构化数据处理开源库SPLMay 24, 2022 pm 01:34 PM

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于结构化数据处理开源库SPL的相关问题,下面就一起来看一下java下理想的结构化数据处理类库,希望对大家有帮助。

Java集合框架之PriorityQueue优先级队列Java集合框架之PriorityQueue优先级队列Jun 09, 2022 am 11:47 AM

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于PriorityQueue优先级队列的相关知识,Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,下面一起来看一下,希望对大家有帮助。

完全掌握Java锁(图文解析)完全掌握Java锁(图文解析)Jun 14, 2022 am 11:47 AM

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于java锁的相关问题,包括了独占锁、悲观锁、乐观锁、共享锁等等内容,下面一起来看一下,希望对大家有帮助。

一起聊聊Java多线程之线程安全问题一起聊聊Java多线程之线程安全问题Apr 21, 2022 pm 06:17 PM

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于多线程的相关问题,包括了线程安装、线程加锁与线程不安全的原因、线程安全的标准类等等内容,希望对大家有帮助。

详细解析Java的this和super关键字详细解析Java的this和super关键字Apr 30, 2022 am 09:00 AM

本篇文章给大家带来了关于Java的相关知识,其中主要介绍了关于关键字中this和super的相关问题,以及他们的一些区别,下面一起来看一下,希望对大家有帮助。

Java基础归纳之枚举Java基础归纳之枚举May 26, 2022 am 11:50 AM

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于枚举的相关问题,包括了枚举的基本操作、集合类对枚举的支持等等内容,下面一起来看一下,希望对大家有帮助。

java中封装是什么java中封装是什么May 16, 2019 pm 06:08 PM

封装是一种信息隐藏技术,是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法;封装可以被认为是一个保护屏障,防止指定类的代码和数据被外部类定义的代码随机访问。封装可以通过关键字private,protected和public实现。

归纳整理JAVA装饰器模式(实例详解)归纳整理JAVA装饰器模式(实例详解)May 05, 2022 pm 06:48 PM

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于设计模式的相关问题,主要将装饰器模式的相关内容,指在不改变现有对象结构的情况下,动态地给该对象增加一些职责的模式,希望对大家有帮助。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
2 週前By尊渡假赌尊渡假赌尊渡假赌
倉庫:如何復興隊友
1 個月前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒險:如何獲得巨型種子
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

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