深入談談String.intern在JVM的實現
前言
String 類的方法可能大家比較少用也比較陌生,雖然實際項目中並不太建議使用方法,可以在 Java 層來實現類似的池,但我們還是要知道它的原理機制不是。
關於intern方法
通過該方法可以返回一個字元串標準對象,JVM 有一個專門的字元串常量池來維護這些標準對象,常量池是一個哈希 map 結構,字元串對象調用方法會先檢查池中是否已經存在該字元串的標準對象,如果存在則直接返回標準對象,如果不存在則會往池中創建標準對象並且返回該對象。
查找過程是使用字元串的值作為 key 進行的,也就是說對於相同的字元串值獲取到的都是同一個標準對象,比如在 Java 層可以有多個字元串值為「key1」的字元串對象,但通過方法獲取到的都是同一個對象。
有什麼作用
那麼方法有什麼作用呢?前面我們知道了 Java 層只要字元串的值相等那麼通過獲取到的一定是同一個對象,也就是所謂的標準對象。比如下面,
發現了嗎?我們竟然能用來對比兩個對象的值了,要知道在 Java 中這樣比較只能判斷它們是否為同一個引用的,但通過方法處理後就可以直接這樣對比了,比起可是快很多啊,性能蹭蹭漲。你可能會說是啊,那是因為已經做了類似的比較操作了啊,這裡照樣會很耗時的好嘛!是的,你說的沒錯,但假如我後面要進行多次比較,那是不是就體現出優勢來了,只要做一次後面比較全部都可以用進行快速比較了。
另外,某些場景下也能達到節省內存的效果,比如要維護大量且可能重複的字元串對象,比如十萬個字元串對象,而字元串值相同的有九萬個,那麼通過方法就可以將字元串對象數減少到一萬,值相同的都共用同一個標準對象。
加入運行時常量池
在 Java 層有兩種方式能將字元串對象加入到運行時常量池中:
在程序中直接使用雙引號來聲明字元串對象,執行時該對象會被加入到常量池。比如下面,該類被編譯成位元組碼後在運行時有相應的指令會將其添加到常量池中。
另外一種是通過 String 類的方法,它能檢測常量池中是否已經有當前字元串存在,如果不存在則將其加入到常量池中。比如下面,
再來個例子
JDK9。
常量池的實現
Java 層很簡單,僅僅將定義為本地方法。
對應為函數,主要先通過函數轉成 JVM 層的 oop 指針,再調函數獲得最終返回的對象,最後再通過轉換成 Java 層的對象並返回。
主要看,StringTable 就是 JVM 運行時用來存放常量的常量池。它的結構為一個哈希 Map,大致如下圖所示,
主要邏輯是先計算 utf-8 編碼的字元串對應的 unicode 編碼的長度,按照 unicode 編碼所需的長度創建新的數組並將字元串轉換成 unicode 編碼,最後再調另外一個函數。
邏輯如下,
通過得到哈希值。
根據哈希值調用函數查找查看共享哈希表中是否已經有這個值的字元串對象,如果有則直接返回找到的對象,該函數會間接調用函數,後面會進一步分析。
是否使用了其他哈希演算法,是的話重新計算哈希值。
通過函數計算哈希值對應的索引值。
通過函數到對應索引值的桶內去查找字元串對象,如果找到就返回該對象。
以上都沒在哈希表中找到的話則需要添加到表中了,用加鎖,然後調用函數完成添加操作,該函數後面會進一步分析。
返回字元串對象。
常量池是一個哈希表,那麼它默認的桶的數量是多少呢?看下面的定義,64位系統上默認為 60013,而32位的則為 1009。
查找哈希表的邏輯為,
哈希值對桶數取余得到索引。
通過索引獲取對應的桶信息。
獲取桶的偏移。
獲取桶的類型。
獲取 entry。
如果是類型的桶,則直接解碼偏移量對應的對象,該類型的 entries 中每個 entry 只有一個4位元組用來表示偏移量,即。
如果是普通類型的桶,則遍歷 entry 找到指定哈希值的 entry 對應的偏移量,然後解碼偏移量對應的對象。其中entries 中每個 entry 有8個位元組,結構為,前面為哈希值,後面為偏移或字元對象指針。
兩種不同類型的結構可以由以下簡單展示,第一個桶和第三個桶是普通類型,指向[哈希+偏移量]組成的很多 entry ,而第二個桶是類型,直接指向[偏移量]。
添加哈希表的邏輯如下,
是否使用了其他哈希演算法,是則重新計算哈希值,並計算對應的索引值。
通過函數檢查哈希表中是否已經存在字元串值。
創建 entry ,包括了哈希值和字元串對象指針。
通過函數添加到哈希表中。
返回字元串對象。
-XX:StringTableSize
前面說了 JVM 默認的情況下的哈希表的桶大小為:64位系統為 60013,而32位的則為 1009。如果我們要改變它的大小,可以通過設置來達到效果。
-XX:+PrintStringTableStatistics
如果你想看常量池相關的統計,可以設置,那麼 JVM 停止時就會輸出相關信息了。比如,
TAG:遠洋號 |