提高 Java 程式碼效能的各種技巧
Java 6,7,8 中的 String.intern – 字串池
這篇文章將要討論 Java 6 中是如何實現 String.intern
方法的,以及這個方法在 Java 7 以及 Java 8 中做了哪些調整。
字串池
字串池(有名字串標準化)是通過使用唯一的共享 String
物件來使用相同的值不同的地址表示字串的過程。你可以使用自己定義的 Map<String, String>
(根據需要使用 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<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程式碼效能的簡單技巧 - levelupJava
- 有助於改善效能的Java程式碼技巧Java
- 三種提高Python程式碼效能的簡便方法Python
- 程式設計技巧│提高 Javascript 程式碼效率的技巧程式設計JavaScript
- Java中各種線性表的效能分析Java
- 提高java和mysql程式碼效能和質量薦JavaMySql
- Java效能優化:教你提高程式碼執行的效率Java優化
- 提高 ASP.NET Web 應用效能的 24 種方法和技巧ASP.NETWeb
- 提高HTML5Canvas效能的技巧HTMLCanvas
- 提高 PostgreSQL 插入效能的 5 個技巧SQL
- 提高程式碼顏值的幾個小技巧
- 提高程式碼質量的12個技巧
- 提高PHP程式碼質量的36個技巧PHP
- 提高jQuery程式碼效能的幾個方法jQuery
- 提高jquery程式碼效能的幾個措施jQuery
- JAVA各種OOM程式碼樣例及解決方法JavaOOM
- 提高java程式效能之垃圾收集 (轉)Java
- 五種提高SQL效能的方法SQL
- 五種提高 SQL 效能的方法SQL
- JAVA的各種OJava
- 如何使用 Set 來提高程式碼的效能
- chrome開發者工具各種騷技巧Chrome
- 提高jquery效能的常用技巧簡單介紹jQuery
- 如何提高 Java 中鎖的效能Java
- js程式碼優化 提高執行效能JS優化
- 如何提高Java程式碼質量-優雅的寫程式碼Java
- Java 中的5個程式碼效能提升技巧,最高提升近10倍Java
- Python 程式碼效能優化技巧Python優化
- 程式猿的年終總結,各種版本各種殘
- 良好的編碼習慣 —— 5 個提高程式碼質量的技巧
- 提高程式碼可讀性的 10 個註釋技巧
- Java中各種Log的使用Java
- Java 各種鎖的小結Java
- 各種分割線Html程式碼(厲害)HTML
- 利用函式的惰性載入提高 javascript 程式碼效能函式JavaScript
- 前端優化:9 個技巧,提高 Web 效能前端優化Web
- 前端優化 9 個技巧,提高 Web 效能前端優化Web
- Java程式設計提高效能時需注意的地方Java程式設計