標籤:
原文連結: java-performance 翻譯: ImportNew.com - 夏千林
譯文連結: http://www.importnew.com/7656.html
ChangeLog:
- 2013年11月19日,更新了Java8版本中的變化。
- 013年11月28日,更新了Java 7u40版本中的變化。(感謝Sunny Chan以及他的同事提示我關注新版本的JDK)
共用一個基礎char[]
原先的String類中有4個非靜態變數:
- char[] value用於儲存字串。
- int offset用於記錄字串首字母在value數組中對應的下標。
- int count用於記錄字串的長度。
- int hash用於緩衝該字串的雜湊值。
正如你所看到的,絕大多數String對象都會是offset=0並且count=value.length。除非通過調用String.substring方法建立的String對象,或是間接調用Pattern.split這類API建立對象。
String.substring建立的String對象將和原String對象共用同一個內部變數char[] value,這樣設計的好處是:
- 通過共用字串節省記憶體開銷。
- String.substring方法的時間複雜度為O(1)。
然而,這樣的設計有可能會導致記憶體泄露:如果你從一個長度很長的String對象中提取出一個很短的子串,當這個String對象不再需要時(該對象靜候GC回收),你的子串中還保持著這個String對象中儲存著完整字串的char[] value數組的引用。這種情況的解決方案是通過建構函式new String(String)建立一個新的子串對象,進而解除短子串與長母串之間的依賴關係。
自Java 1.7.0_06版本起(包括Java 8的最新版本),String類中不再有offset和count變數。這意味著成員變數char[] value將不會被共用。你可以忘記上述有關記憶體泄露的描述以及如何使用new String(String)方法來避免記憶體泄露的發生。但需要記住的是,String.substring現在是線性級的時間複雜度,不再是常數級的時間複雜度。
雜湊演算法的改變
下面的部分僅適用於Java 7u6以上的Java 7版本,這些代碼在Java 8中已被刪除。
String類在本次更新中的另一個變化是一個新的雜湊演算法。Oracle表示新的演算法能產生出更好的雜湊分布,並且能夠提高基於雜湊演算法的容器的效能,如HashMap、Hashtable、HashSet、 LinkedHashMap、LinkedHashSet、WeakHashMap和ConcurrentHashMap 。與本文第一部分介紹的變化不同,這部分變化是實驗性質的,預設是關閉的。
你可能已經猜到了,這部分變化只適用於String類型的Key。如果想要啟用它們,需要將系統變數jdk.map.althashing.threshold設定為一個非負的整數值(預設為-1)。當使用新的雜湊方法時,這個值將是容器大小的閾值。這裡需要注意的是:雜湊方法只有在進行重雜湊(rehash)的時候才會被更新。因此,如果容器上次執行重雜湊是在size=160的時候,而jdk.map.althashing.threshold = 200,這樣只有在容器的size增長到大約320的時候,雜湊方法才會被更新。
String類現在有一個hash32()方法,它的結果被緩衝在成員變數int hash32 中。這個方法最大的變化是,同一個字串在不同的JVM上執行hash32()的結果可能不同(確切的說,多數情況下都會不同,因為其內部分別調用了一次System.currentTimeMillis()和兩次System.nanoTime()用於初始化seed)。因此,某些容器在每次運行程式時迭代順序都不同。
事實上,我對這個方法的改變有一點意外。如果原先的hashCode方法啟動並執行很好,為什麼我們需要一個新的雜湊方法呢?我決定使用文章hashcode方法效能調優中的測試程式,測試一下使用hash32方法會產生多少個重複的雜湊值。
String.hash32()方法不是公有的,因此我只能通過查看HashMap的源碼來找到調用String.hash32()的方法。答案是issun.misc.Hashing.stringHash32(String)。
使用同一資料集(由1百萬個各不相同的Key組成)進行測試,String.hash32產生了304個重複的雜湊值,而相比之下String.hashCode並沒有產生重複的雜湊值。我想我們需要靜候Oracle進一步的完善或者更多的使用情境說明。
新的雜湊演算法可能會嚴重影響高並發、多線程代碼
本章節適用於Java 7版本的build 6(包含build 6)至build40(不包含build40)。這部分代碼在Java 8中已被刪除。有關Java 7u40以上版本的相關介紹請參見下一個章節。
Oracle在後面這些類的雜湊實現中遺留了一個bug: HashMap、Hashtable、HashSet、LinkedHashMap、LinkedHashSet和WeakHashMap,只有ConcurrentHashMap 不受影響。這是因為所有的非concurrent類中現在都引入了下面的成員變數:
| 12345 |
/** * A randomizing value associated with this instance that is applied to * hash code of keys to make hash collisions harder to find. */transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this); |
這意味著每一個map或set執行個體建立的過程中都會調用sun.misc.Hashing.randomHashSeed方法。randomHashSeed後續會調用java.util.Random.nextInt方法。Random類以其多線程環境下不友好而聞名:它有一個Atomic類型的成員變數private final AtomicLong seedfield。Atomic類型在多線程競爭程度較低或者適中的情境下效能表現較好,但在競爭激烈的情境下效能很差。
因此,很多處理HTTP/JSON/XML請求的高負載Web應用可能會被這個bug所影響,因為現有的解析器在表示名值對(name-value)時幾乎都使用了上述的存在bug的容器。這些解析器還很可能使用了嵌套的map,這會進一步增加每秒中建立map執行個體的數量。
如何解決這一問題呢?
1. 使用ConcurrentHashMap :只有在設定系統變數jdk.map.althashing.threshold時才會調用randomHashSeed 方法。但很可惜的是,這種方式僅適用於JDK的核心開發人員。
| 12345678910111213 |
/** * A randomizing value associated with this instance that is applied to * hash code of keys to make hash collisions harder to find. */private transient final int hashSeed = randomHashSeed(this); private static int randomHashSeed(ConcurrentHashMap instance) { if (sun.misc.VM.isBooted() && Holder.ALTERNATIVE_HASHING) { return sun.misc.Hashing.randomHashSeed(instance); } return 0;} |
2. Hacker的方式:修改sun.misc.Hashing類,這種方式極度不推薦。但如果你依然想解決這個bug,解決的思路是:java.util.Random類並不是final的。你可以在Java 7中加入Random 類的一個Thread Local的子類:java.util.concurrent.ThreadLocalRandom,它內部使用了ThreadLocal<ThreadLocalRandom>(感謝Benjamin Possolo指出我在之前的文章中遺漏了這個類的介紹)。除此之外,ThreadLocalRandom屬於CPU cache感知型:每個ThreadLocalRandom執行個體的seed後面增加了64位元組的填充(cache行的大小),進而降低2個不同的seed在同一個cache行中碰撞的可能性。
然後你可以修改成員變數sun.misc.Hashing.Holder.SEED_MAKER,將它初始化為Random子類的執行個體(ThreadLocalRandom)。不用擔心這個變數是私人的、靜態而且是final的,反射機制可以幫你。
| 123 |
public class Hashing { private static class Holder { static final java.util.Random SEED_MAKER; |
Java 7u40以上的版本中新的雜湊演算法不再影響高並發、多線程代碼
Oracle在Java 7u40版本中修正了上述的bug。
他們使用了上一章節中提到的方法一,僅在重雜湊閥值被啟用時(通過設定系統變數jdk.map.althashing.threshold啟用)才調用sun.misc.Hashing.randomHashSeed方法。Oracle只修改了兩個類:HashMap和Hashtable,進而間接修改了 HashSet、LinkedHashMap和LinkedHashSet,因為這三個類是基於HashMap實現的。唯一沒有被修改的類是WeakHashMap,但我實在想不出這個類會被大量執行個體化的應用情境。
相關文章
最近,本文在Reddit上引起了激烈的討論。我推薦讀者們去看一看:
- Reddit使用者 bondolo的一篇文章探討這些變化背後的原因 。
- Java 6、7、8中的String.intern方法:String池
總結
- 自Java 1.7.0_06版本起,String.substring方法會為每個子串建立一個新的char[] value(而不是共用母串的char[] value)。這意味著String.substring方法的時間複雜度由常數階變為線性階。這種變化的好處是String對象佔用的記憶體稍微少了一些(比以前少8個位元組),同時確保String.substring方法不會導致記憶體流失(有關Java對象記憶體布局的詳細資料,請見String packing part 1: converting characters to bytes)。
- Java 7u6+版本中的功能,在Java 8中被刪除。自Java 1.7.0_06版本起,String類有了第二個雜湊函數:hash32。該方法目前還不是公有的,只能通過使用反射機制或者是調用sun.misc.Hashing.stringHash32(String)來訪問該方法。只有當那7種雜湊相關的JDK容器的大小超過系統變數jdk.map.althashing.threshold所設定的閥值時,該方法才會被使用。這是一個實驗性質的功能,目前我不推薦在代碼中使用這一功能。
- Java 7u6 (包含Java 7u6)至Java 7u40(不包含Java 7u40)版本中的功能,不適用於Java 8。新的雜湊實現引入了一個效能上的bug,這個bug涉及Java 7u6 (包含Java 7u6)到Java 7u40(不包含Java 7u40)之間所有版本中所有標準的非concurrent的Map和Set容器。這個bug隻影響多線程應用每秒鐘建立Map執行個體的效率。詳情請見本文第三章節。Java 7u40版本已修複這個bug。
Java 1.7.0_06中String類內部實現的一些變化【轉】