分層理解Java字符串常量池

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)方式不盡相同霍转。

image

我們將在此思想基礎(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ù)常識和上面的描述推測出一個簡單模型:

image

按照這個模型愧旦,可以推出幾個簡單的判斷

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ù)完善模型:

image

按照這個模型谤辜,可以推出幾個簡單的判斷:

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ù)探究。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末膘魄,一起剝皮案震驚了整個濱河市乌逐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌创葡,老刑警劉巖浙踢,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異灿渴,居然都是意外死亡洛波,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門骚露,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹬挤,“玉大人,你說我怎么就攤上這事棘幸⊙姘猓” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵误续,是天一觀的道長吨悍。 經(jīng)常有香客問我,道長蹋嵌,這世上最難降的妖魔是什么育瓜? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮栽烂,結(jié)果婚禮上躏仇,老公的妹妹穿的比我還像新娘恋脚。我一直安慰自己,他們只是感情好钙态,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布慧起。 她就那樣靜靜地躺著菇晃,像睡著了一般册倒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上磺送,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天驻子,我揣著相機與錄音,去河邊找鬼估灿。 笑死崇呵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的馅袁。 我是一名探鬼主播域慷,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汗销!你這毒婦竟也來了犹褒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤弛针,失蹤者是張志新(化名)和其女友劉穎叠骑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體削茁,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡宙枷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了茧跋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慰丛。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖瘾杭,靈堂內(nèi)的尸體忽然破棺而出诅病,到底是詐尸還是另有隱情,我是刑警寧澤富寿,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布睬隶,位于F島的核電站,受9級特大地震影響页徐,放射性物質(zhì)發(fā)生泄漏苏潜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一变勇、第九天 我趴在偏房一處隱蔽的房頂上張望恤左。 院中可真熱鬧贴唇,春花似錦、人聲如沸飞袋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽巧鸭。三九已至瓶您,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纲仍,已是汗流浹背呀袱。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留郑叠,地道東北人夜赵。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像乡革,于是被迫代替她去往敵國和親寇僧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容