[導讀] 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 中是如何實作String.intern
方法的,以及這個方法在Java 7 以及Java 8 中做了哪些調整。
字串池(有名字串標準化)是透過使用唯一的共享String
物件來使用相同的值不同的地址表示字串的過程。你可以使用自己定義的<a href="http://www.php.cn/code/8210.html" target="_blank">Map</a>05ad6303f369fc4ccec4412db2772d19
(根據需要使用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
在美好的過去所有共享的String 物件都儲存在PermGen 中— 堆中固定大小的部分主要用於儲存載入的類別物件和字串池。除了明確的共享字串,PermGen 字串池還包含所有程式中使用過的字串(這裡要注意是使用過的字串,如果類別或方法從未載入或被條用,在其中定義的任何常數都不會被載入)
Java 6 中字串池的最大問題是它的位置— PermGen。 PermGen 的大小是固定的並且在運行時是無法擴展的。你可以使用 -XX:MaxPermSize=N
配置來調整它的大小。據我了解,對於不同的平台預設的 PermGen 大小在 32M 到 96M 之間。你可以擴展它的大小,不過大小使用都是固定的。這個限制需要你在使用 String.intern
時需要非常小心 — 你最好不要使用這個方法 intern 任何無法控制的使用者輸入。這就是為什麼在JAVA6 中大部分使用手動管理Map
來實作字串池
Java 7 中Oracle 的工程師對字串池的邏輯做了很大的改變— 字串池的位置被調整到heap 中了。這意味著你再也不會被固定的記憶體空間限制了。所有的字串都保存在堆(heap)中同其他普通物件一樣,這使得你在調優應用時只需要調整堆大小。這 個改動使得我們有足夠的理由讓我們重新考慮在 Java 7 中使用 String.intern()。
沒錯,在 JVM 字串池中的所有字串會被垃圾收集,如果這些值在應用中沒有任何引用。這是用於所有版本的 Java,這意味著如果 interned 的字串在作用域外並且沒有任何引用 — 它將從 JVM 的字串池中被垃圾收集掉。
因為被重新定位到堆中以及會被垃圾收集,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
在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 字串池和 WeakHashMap5e31d6cc56a10347ef5ebe29e4b04285>
它可以用來模擬 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倍多)。我認為,這非常值得你在你的應用程式中去掉所有手工字串池。
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中文網其他相關文章!