Java是一門計算機編程語言卵佛,但我們腦海中所理解的Java不僅僅是一門語言杨赤。它還包括Java虛擬機(JVM)的一系列規(guī)定,及具體Java產(chǎn)品(如Hotspot)的實現(xiàn)原理截汪。
不管我們?nèi)粘T贘ava中用到的任何一種語法疾牲,都會由語言規(guī)范對其進(jìn)行語義和用法上的規(guī)定,再由虛擬機規(guī)范進(jìn)行實現(xiàn)方案上的約束和建議衙解,最后由具體的產(chǎn)品進(jìn)行編碼實現(xiàn)阳柔。其中,語言規(guī)范和虛擬機規(guī)范是 Oracle 制定好的(https://docs.oracle.com/javase/specs/index.html)蚓峦,不同的Java產(chǎn)品(如Hotspot舌剂、JRockit、J9)等暑椰,對虛擬機規(guī)范的實現(xiàn)方式不盡相同霍转。
我們將在此思想基礎(chǔ)上,探究Java中字符串常量池的概念一汽,及字符串的對象創(chuàng)建避消、引用、“駐留”(intern)等一系列操作召夹,及由此引申的Java加載岩喷、鏈接、初始化步驟戳鹅。
Java語言層面
在Java語言標(biāo)準(zhǔn)中均驶,沒有提及字符串常量池,但是有對于“字符串字面常量”的定義:
3.10.5. 字符串字面量
字符串字面常量是用由雙引號括起來的0個或多個字符構(gòu)成的枫虏,字符串字面常量的類型總是String,是對String類的實例的引用爬虱。
...
一個字符串字面常量總是引用String類的同一個實例隶债。這是因為其被通過使用String.intern() 方法而“駐留”了(直譯應(yīng)為“限定”,但是“駐留”更能體現(xiàn)字符串常量池的存在)跑筝,這樣做是為了讓它們可以共享唯一的實例死讹。
從這段表述中可以得出幾點結(jié)論:
- 字符串字面量的指向不會發(fā)生變化,被指向的實體是“先到先得”的曲梗;
- 不同的 String 對象可以通過 String.intern() 方法得到同一個字符串字面量赞警,即同一個 String 對象的引用妓忍;
盡管標(biāo)準(zhǔn)中沒有提到字符串常量池,但姑且根據(jù)常識和上面的描述推測出一個簡單模型:
按照這個模型愧旦,可以推出幾個簡單的判斷
String a = "xyz";
String b = new String("xyz");
System.out.println(a==b); //false
System.out.println(a==a.intern()); //true
System.out.println(a==b.intern()); //true
System.out.println(b==b.intern()); //false
為什么a = "xyz"
和b = new String("xyz")
對應(yīng)的是兩個對象呢世剖,Java語言標(biāo)準(zhǔn)中有對創(chuàng)建類實例的標(biāo)準(zhǔn)描述 :
12.5. 創(chuàng)建新的類實例
新的類實例在類實例創(chuàng)建表達(dá)式的計算導(dǎo)致類被實例化時顯式地創(chuàng)建。
新的類實例可以在下列情況下隱式地創(chuàng)建:
- 加載包含String字面常量的類或接口時笤虫,會創(chuàng)建新的String對象旁瘫,用來表示該字面常量。(如果同一個String對象之前已經(jīng)被駐留了琼蚯,那么這里就不會再創(chuàng)建新的String對象了)酬凳;
- 執(zhí)行不是常量表達(dá)式的字符串連接操作符時,有時會創(chuàng)建新的String對象以表示執(zhí)行結(jié)果遭庶。
從這段表述中可以得出幾點結(jié)論:
-
String a = new String()
這種顯式創(chuàng)建的寫法宁仔,必定會在堆中創(chuàng)建一個新的對象 -
String a = "xyz"
這種寫法一般情況下會在類加載過程中隱式在堆中創(chuàng)建一個新的對象并將字面量“xyz”駐留,但是若"xyz"字面量已經(jīng)被駐留的話峦睡,則不創(chuàng)建新對象 -
String a = new String("xy") + new String("z")
台诗,這種通過"+"連接的寫法,運行時會創(chuàng)建一個新的String對象表示結(jié)果赐俗,但是并不會將"xyz"駐留(這里要注意了) - 但如果是一個常量表達(dá)式 (§15.29)
String a = "xy" + "z"
則不同拉队,它與String a = "xyz"
一樣,對應(yīng)的"xyz"字面量已在類加載過程中被駐留(interned)了(可以理解為編譯期間已經(jīng)被優(yōu)化為了"xyz")阻逮,且運行時不會創(chuàng)建新對象
這里再重點強調(diào)一遍粱快,對于String a = "xyz"
,在類加載過程中叔扼,就已經(jīng)生成了一個代表"xyz"的對象事哭,在運行這行代碼時僅僅是從字符串常量池中獲取了該對象的引用并返回,但是對于String a = new String("xyz")
瓜富,雖然在類加載過程中就已經(jīng)生成了一個代表"xyz"的對象鳍咱,但是由于是顯式的使用了new
關(guān)鍵字,所以仍會創(chuàng)建一個新的對象
那么根據(jù)這幾點表述与柑,繼續(xù)完善模型:
按照這個模型谤辜,可以推出幾個簡單的判斷:
String a = "xyz";
String b = new String("xyz");
String c = "xy"+"z";
String d = "xyz";
String e = new String("xy")+"z";
System.out.println(a==b); //false
System.out.println(a==c); //true
System.out.println(a==d); //true
System.out.println(a==e); //false
整理已經(jīng)介紹過的標(biāo)準(zhǔn),可以給出一個推論价捧,當(dāng)且僅當(dāng)出現(xiàn)以下三種情況下丑念,字面量才有可能會駐留(interned):
- 代碼中出現(xiàn)被引號包含的字面量,如:String a = "xyz"结蟋,字面量"xyz"在類加載過程被駐留
- 代碼中出現(xiàn)String類型的常量表達(dá)式脯倚,如:String a = "xy" + "z",字面量"xyz"在類加載過程被駐留
- 調(diào)用了intern()方法嵌屎,如 a.intern()推正,若a對應(yīng)的字面量沒有被駐留過恍涂,則駐留該字面量,否則返回之前駐留的字面量(即對象引用)
可以結(jié)合以下例子理解:
// 字面量abc既未被引號包圍植榕,也不是一個常量表達(dá)式再沧,僅創(chuàng)建對象
String f = new String("ab") + new String("c");
// 將字面量abc駐留(即f對象引用)
f.intern();
// 字面量abc被引號包圍,類加載過程中内贮,發(fā)現(xiàn)字面量abc已被駐留产园,則直接返回f對象引用
String g = "abc";
// true
System.out.println(f == g);
這里要注意一點:f.intern()
與f = f.intern()
是不一樣的,f.intern()
并不更改f本身的值夜郁。
到這里什燕,可以看出,java語言層面中盡管沒有提到字符串常量池這個字眼竞端,但是對駐留(interned)這個概念的解釋已經(jīng)非常通透了屎即,根據(jù)以上根據(jù)標(biāo)準(zhǔn)的推論,所有字符串的 == 問題都能夠得到答案事富,接下來介紹語言層面之下的細(xì)節(jié)技俐。
JVM層面
在JAVA語言標(biāo)準(zhǔn)中,定義字面量是被引號包圍的東西统台,實際上是一個對象引用雕擂,那么JVM標(biāo)準(zhǔn)層面是如何描述字面量這個概念的呢:
5.1. 運行時常量池
字符串常量是指向String類實例的引用,它來自于類或接口二進(jìn)制表示中的 CONSTANT_String_info 結(jié)構(gòu)贱勃。其給出了由Unicode碼點序列所組成的字符串常量井赌。
Java語言規(guī)定,相同的字符串常量必須指向同一個String類實例贵扰。此外仇穗,如果在任一字符串上調(diào)用String.intern方法,那么其返回結(jié)果所指向的那個類實例戚绕,必須和直接以常量形式出現(xiàn)的字符串實例完全相同纹坐。為了得到字符串常量,Java虛擬機需要檢查 CONSTANT_String_info 結(jié)構(gòu)中的碼點序列:
- 如果某String實例所包含的Unicode碼點序列與 CONSTANT_String_info 結(jié)構(gòu)所給出的序列相同舞丛,而之前又曾在該實例上面調(diào)用過 String.intern 方法耘子,那么此次字符串常量獲取的結(jié)果將是一個指向相同String實例的引用;
- 否則瓷马,會創(chuàng)建一個新的String實例拴还,其中包含由 CONSTANT_String_info 結(jié)構(gòu)所給出的Unicode碼點序列;字符串常量獲取的結(jié)果是指向那個新String實例的引用欧聘,最后,新String實例的intern方法被Java虛擬機自動調(diào)用端盆。
4.4.3. CONSTANT_String_info 結(jié)構(gòu)
CONSTANT_String_info structure 用于表示String類型的常量對象怀骤,其結(jié)構(gòu)如下:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}CONSTANT_String_info 結(jié)構(gòu)各項說明如下:
- tag:值為 CONSTANT_String (8)费封;
- string_index:必須是對常量池標(biāo)的有效索引,常量池表在該索引出的成員必須是 CONSTANT_Utf8_info 結(jié)構(gòu)蒋伦,此結(jié)構(gòu)表示Unicode碼點序列弓摘,此序列最終會被初始化為一個String對象。
4.4.7. CONSTANT_Utf8_info 結(jié)構(gòu)
CONSTANT_Utf8_info 結(jié)構(gòu)用來表示字符串常量的值:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
這段描述中痕届,string constant
就是java語言標(biāo)準(zhǔn)中的字面量(string literals)韧献,對其特性又描述了一遍,基本與java標(biāo)準(zhǔn)中描述的一致研叫,但是提供了更多的細(xì)節(jié):
- 字面量是通過運行時常量池中的
CONSTANT_String_info
結(jié)構(gòu)來獲取的 -
CONSTANT_String_info
結(jié)構(gòu)只持有了一個CONSTANT_Utf8_info
結(jié)構(gòu)在運行時常量池中的index -
CONSTANT_Utf8_info
持有了一個Unicode字節(jié)序列锤窑,JVM可以通過這個序列來做唯一性檢測
通過上面的幾點描述,字符串常量池的輪廓已經(jīng)差不多了嚷炉,規(guī)范中說要通過CONSTANT_String_info
來獲取字面量渊啰,那怎么獲取呢,很容易推斷出JVM中應(yīng)該存在一個類似HashMap的結(jié)構(gòu)申屹,key可能是根據(jù)CONSTANT_String_info
獲取到的CONSTANT_Utf8_info
中的Unicode字節(jié)序列(bytes[length]
)(這里只是一個猜測绘证,實際如何得看HotSpot層面的實現(xiàn)了),value就是字面量(對象引用)
至于CONSTANT_Utf8_info
究竟是個什么東西哗讥,就得看具體JVM是如何實現(xiàn)的了嚷那,對于Hotspot而言,所謂的Unicode字節(jié)序列可能就是個C++的bytes數(shù)組杆煞。
Java產(chǎn)品實現(xiàn)層面
Java的實現(xiàn)魏宽,我們?nèi)粘=佑|最多的就是Hotspot。我們以Hotspot為例了解索绪。
回顧一下JAVA語言層面與JVM標(biāo)準(zhǔn)層面的描述:JVM可以通過類運行時常量池中的CONSTANT_String_info
來找到對應(yīng)的字面量(即對象引用)湖员,于是我們假設(shè)存在一個類似HashMap結(jié)構(gòu)的字符串常量池來輔助完成這件事情,那么真的有這么個結(jié)構(gòu)嗎瑞驱,可以從HotSpot代碼中找到答案:
HotSpot VM里實現(xiàn)字符串常量池功能的是StringTable類娘摔,在hotspot/src/share/vm/classfile/symbolTable.[hpp|cpp]中,看它的定義:
class StringTable : public Hashtable<oop, mtSymbol>
在C++層面的確是個Hashtable類型唤反,key是oop
類型(oop類型就是Java層面的對象引用)凳寺,value是mySymbol
類型,乍眼一看不知道是什么東西彤侍,那么可以從向這個Stringtable里插入的代碼入手:
oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
int len, unsigned int hashValue_arg, TRAPS) {
// ...
// Check if the symbol table has been rehashed, if so, need to recalculate
// the hash value and index before second lookup.
unsigned int hashValue;
int index;
if (use_alternate_hashcode()) {
hashValue = hash_string(name, len);
index = hash_to_index(hashValue);
} else {
hashValue = hashValue_arg;
index = index_arg;
}
// Since look-up was done lock-free, we need to check if another
// thread beat us in the race to insert the symbol.
oop test = lookup(index, name, len, hashValue); // calls lookup(u1*, int)
if (test != NULL) {
// Entry already added
return test;
}
HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
add_entry(index, entry);
return string();
}
首先根據(jù)jchar* name
計算出hash值肠缨,轉(zhuǎn)化賦值給index,然后調(diào)用了lookup
方法去判斷StringTable
中是否已經(jīng)存在對應(yīng)的字面量盏阶,結(jié)合JVM規(guī)范來看晒奕,jchar* name
這個入?yún)?yīng)該就是運行時常量池中CONSTANT_Utf8_info
類型變量持有的Unicode字節(jié)序列(bytes[length]
)
再看lookup函數(shù):
oop StringTable::lookup(int index, jchar* name,
int len, unsigned int hash) {
int count = 0;
for (HashtableEntry<oop, mtSymbol>* l = bucket(index); l != NULL; l = l->next()) {
count++;
if (l->hash() == hash) {
if (java_lang_String::equals(l->literal(), name, len)) {
return l->literal();
}
}
}
// If the bucket size is too deep check if this hash code is insufficient.
if (count >= BasicHashtable<mtSymbol>::rehash_count && !needs_rehashing()) {
_needs_rehashing = check_rehash_table(count);
}
return NULL;
}
邏輯非常簡單,根據(jù)index找到了hashtable對應(yīng)的拉鏈節(jié)點的位置,然后逐個對節(jié)點的key進(jìn)行判斷脑慧,對比jchar* name
是否與對象表示的字符串相同(java_lang_String::equals
),若一致魄眉,則直接把key(對象引用)返回。
所以闷袒,StringTable的結(jié)構(gòu)就可以當(dāng)成是個簡單的hashtable(拉鏈?zhǔn)剑┛勇桑琱ashcode是根據(jù)對應(yīng)的Unicode字節(jié)序列計算而來,節(jié)點中存放的就是對象引用(字面量)囊骤。
綜上晃择,我們從不同層次介紹了字符串常量池的概念和實現(xiàn)。實際上我們主要需要聚焦Java語言層面和JVM層面也物,實現(xiàn)層面可以給我們提供更多的實踐思路宫屠。關(guān)于引出的Java加載、鏈接焦除、初始化的過程激况,以后再繼續(xù)探究。