深入分析String.intern和String常量的實現原理
背景
字串型別在實際應用場景中使用非常頻繁,如果為每個字串常量都生成一個對應的String物件,明顯會造成記憶體的浪費,針對這一問題,虛擬機器實現一個字串常量池的概念,提供瞭如下實現:
1、同一個字串常量,在常量池只有一份副本;
2、通過雙引號宣告的字串,直接儲存在常量池中;
3、如果是String物件,可以通過String.intern方法,把字串常量儲存到常量池中;
本文JVM原始碼版本 openjdk-7-fcs-src-b147-27
疑惑
在不同環境執行上述程式碼,會得到不同的結果,為什麼?
1、JDK1.6的結果:false false
2、JDK1.7的結果:true false
解惑
其中String.intern
在java中是native方法,JDK1.7的註釋如下:
1、執行intern方法時,如果常量池中存在和String物件相同的字串,則返回常量池中對應字串的引用;
2、如果常量池中不存在對應的字串,則新增該字串到常量中,並返回字串引用;
HotSpot1.6實現
常量池的記憶體在永久代進行分配,永久代和Java堆的記憶體是物理隔離的,執行intern方法時,如果常量池不存在該字串,虛擬機器會在常量池中複製該字串,並返回引用,使用intern方法時需要謹慎,避免常量池中字串過多,導致效能變慢,甚至發生PermGen記憶體溢位。
顯然s.intern() == s
不可能成立.
HotSpot1.7實現
intern方法的HotSpot實現入口位於openjdk\jdk\src\share\native\java\lang\String.c
檔案中:
其中JVM_InternString宣告位於openjdk\hotspot\src\share\vm\prims\jvm.cpp
檔案中:
String.intern
StringTable.intern
方法實現,其中StringTable是HotSpot字串常量池的具體實現,1.7的常量池已經在Java堆上分配記憶體。
常量池的初始化
常量池的實現非常簡單,類似JDK中的HashMap,其中StringTable的宣告位於symbolTable.hpp
檔案中:
StringTable最終繼承了BasicHashtable,通過構造方法引數指定常量池的大小StringTableSize,預設為1009,StringTableSize定義在globals.hpp
檔案中:
不過在Java7u40版本之後StringTableSize擴大到了60013,可以通過-XX:StringTableSize = 10009
設定StringTable大小,通過-XX:+PrintFlagsFinal
列印虛擬機器的Global flags引數,可以獲得當前StringTable的大小。
BasicHashtable實現
1、initialize方法初始化常量池的基本值:_table_size、_entry_size等;
2、NEW_C_HEAP_ARRAY方法在堆上分配HashtableBucket;
3、清空StringTable中的HashtableBucket資料;
StringTable.intern實現
1、其中引數string_or_null為指向原字串的控制代碼,name是String物件中字元陣列的拷貝、len為字元陣列的長度;
2、java_lang_String::hash_string
方法計算出字串的hash值,實現如下:
3、BasicHashtable.hash_to_index
方法計算出該hash值在StringTable中桶的位置index,實現如下:
4、StringTable::lookup
方法判斷StringTable指定位置的桶中是否存在相等的字串,實現如下:
lookup方法通過遍歷HashtableEntry連結串列,如果找到對應的hash值,且字串值也相等,說明StringTable中已經存在該字串,則返回該字串引用,否則返回NULL;
5、如果StringTable不存在該字串,則通過StringTable::basic_add
方法新增字串引用到StringTable,實現如下:
basic_add方法中的條件判斷!string_or_null.is_null()
為true,!JavaObjectsInPerm
為true,所以並不會進行字串的複製,而是通過HashtableEntry物件封裝原字串的hash值和指向源字串的控制代碼,新增到StringTable對應bucket的連結串列中,並返回指向原字串控制代碼;其中變數JavaObjectsInPerm預設為false,定義如下:
通過上述分析:HotSpot1.7實現的常量池在java堆上分配記憶體,執行intern方法時,如果常量池已經存在相等的字串,則直接返回字串引用,否則複製該字串引用到常量池中並返回;
1、對於變數s1,常量池中不存在"StringTest"
,所以s1.intern()
和 s1都是指向Java堆上的String物件;
2、對於變數s2,常量池中一開始就已經存在"java"
字串,s2.intern()
方法返回的是另外一個"java"
字串物件,所以s2.intern()
和s2指向的並非同一個物件;
字串常量如何實現?
類似String s = "hello java"
的字串常量宣告,在HotSpot中是如何實現的呢?
其中字串常量"hello java"
會在編譯過程中被儲存在class檔案的Constant pool資料結構中,如下是編譯位元組碼實現:
String s = "hello java"
對應了兩條位元組碼實現:
1、ldc #2
2、astore_1
其中ldc指令的實現在interpreterRuntime.cpp
檔案中,實現如下:
ldc指令中會根據獲取的常量型別進行不同操作,由於目前是字串常量,從而呼叫pool->string_at(index, CHECK)
邏輯,實現如下:
其中h_this是指向當前constantPoolOop例項的控制代碼,最後呼叫string_at_impl
方法:
字串常量一開始以Symbol型別表示,最終通過StringTable::intern
方法生成字串物件,並把字串的真實引用更新到constantPool中,這樣下次執行ldc指令時可以直接