1. 程式人生 > >深入分析String.intern和String常量的實現原理

深入分析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指令時可以直接