Java 基礎:String——常量池與 intern

字符串字面量字符串常量池字符串字面量是什麼時候進入字符串常量池String s=new String("xyz") 涉及到幾個對象String「+」符號的實現參考連接html

Java 中方法區與常量池一節中有講到常量池的分類,以及三種常量池之間的關聯,其中有提到 String 類中的 intern() 方法,能夠在運行期間將 Class 文件常量池中未出現的常量放入到運行時常量池,以及將字符串對象的引用加入到全局字符串常量池中。java

本章節接着上節對 String 源碼的學習,對 String 類中的 intern() 方法進行更加深刻的分析總結。nginx

字符串字面量

字面量一詞我最先是在學習 Class 文件常量池中接觸到的,以前也沒有詳細瞭解過,經查詢相關資料得知,字符串字面量是在 Java™語言規範的 3.10.5. String 字面量中定義的。關於字符串字面量通俗點解釋就是,使用雙引號""建立的字符串,在堆中建立了對象後其引用插入到字符串常量池中(jdk1.7後),能夠全局使用,遇到相同內容的字面量,就不須要再次建立。舉個例子:web

String str1 = "abc";    //運行時會在堆中新建一個「abc」的對象,而後將其引用存入到字符串常量池中,且返回給 str1
String str2 = new String("abc");    //運行時會先去字符串常量池中查看是否有「abc」對象的引用,若是有則不須要建立。以後在堆中建立一個「abc」對象,將該對象的引用返回給 str2
複製代碼

字符串常量池

上一節中主要講述了字符串常量池的存放位置和存放內容,這裏講點更加詳細的內容。面試

首先是字符串常量池中存放內容的驗證,在 jdk6 中,常量池的位置在永久代(方法區)中,此時常量池中存儲的是對象。在 jdk7中,常量池的位置在堆中,此時,常量池存儲的是引用。在 jdk8 中,永久代(方法區)被元空間取代了。下面咱們經過一個例子進行驗證:oracle

String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s3);
System.out.println(s2 == s3);
System.out.println(System.identityHashCode(s1));
System.out.println(System.identityHashCode(s3));


String s4 = new String("3") + new String("3");
String s5 = s4.intern();
String s6 = "33";
System.out.println(s4 == s6);
System.out.println(s5 == s6);
System.out.println(System.identityHashCode(s4));
System.out.println(System.identityHashCode(s6));
複製代碼

執行結果:app

jdk6
false
true
536468534
796216018
false
true
1032010069
1915296511

jdk7
false
true
1163157884
1956725890
true
true
356573597
356573597
複製代碼

爲了更好的解釋,咱們用圖解的方式來分析究竟發生了什麼。jvm

JDK6
ide

String s1 = new String("abc"); 運行時建立了兩個對象,一個是在堆中的」abc「對象,一個是在字符串常量池中的」abc」對象,將堆中對象的地址返回給 s1。post

String s2 = s1.intern(); 在常量池中尋找與 s1 變量內容相同的對象,發現已經存在內容相同對象「abc」,返回該對象的地址,賦值給 s2。

String s3 = "abc"; 首先在常量池中尋找是否有相同內容的對象,發現有,返回對象"abc"的地址,賦值給 s3。

String s4 = new String("3") + new String("3");運行時建立了四個對象,一個是在堆中的「33」對象,一個是在常量池中的「3「對象。中間還有2個匿名的 new String("3") 這裏咱們不去討論它們。

String s5 = s4.intern();在常量池中尋找與 」33「對象內容相同的對象,沒有發現「33」對象,在常量池中建立「33」對象,返回「33」對象的地址給 s5。

String s6 = "33";首先在常量池中尋找是否有相同內容的對象,發現有,返回對象"33"的地址,賦值給 s6。

System.out.println(s4 == s6);從上面能夠分析出,s4 變量和 s6 變量地址指向的不是相同的對象,因此返回 false。

JDK7


String s1 = new String("abc"); 運行時建立了兩個對象,一個是在堆中的」abc「對象,一個是在堆中建立的」abc」對象,並在常量池中保存「abc」對象的引用地址。

String s2 = s1.intern(); 在常量池中尋找與 s1 變量內容相同的對象引用,發現已經存在內容相同對象「abc」的引用,返回該對象引用地址,賦值給 s2。

String s3 = "abc"; 首先在常量池中尋找是否有相同內容的對象引用,發現有,返回對象"abc"的引用地址,賦值給 s3。

String s4 = new String("3") + new String("3");運行時建立了四個對象,一個是在堆中的「33」對象,一個是在堆中建立的」3」對象,並在常量池中保存「3」對象的引用地址。中間還有2個匿名的 new String("3") 這裏咱們不去討論它們。

String s5 = s4.intern();在常量池中尋找與 」33「對象內容相同的對象引用,沒有發現「33」對象引用,將 s4 對應的」33「對象的地址保存到常量池中,並返回給 s5。

String s6 = "33";首先在常量池中尋找是否有相同內容的對象引用,發現有,返回對象"33"的引用地址,賦值給 s6。

System.out.println(s4 == s6);從上面能夠分析出,s4 變量和 s6 變量地址指向的是相同的對象,因此返回 true。

綜上咱們能夠看出,字符串常量池中存放的內容在 jdk6 和 jdk7 中是不同的,前者存放對象,後者存放對象的引用。

爲了弄明白 intern()方法,對於上述的代碼進行調整,來看看結果如何。

String s1 = new String("abc");
String s3 = "abc";
String s2 = s1.intern();
System.out.println(s1 == s3);
System.out.println(s2 == s3);


String s4 = new String("3") + new String("3");
String s6 = "33";
String s5 = s4.intern();
System.out.println(s4 == s6);
System.out.println(s5 == s6);
複製代碼

執行結果:

jdk6
false
true
false
true

jdk7
false
true
false
true
複製代碼

原理很簡單,由於在調用 intern 方法前,先使用了字面量賦值語句,因此在常量池中都存在了與變量相同內容的對象(jdk6)或對象的引用(jdk7+),此時再調用 intern 方法,就會發現常量池裏的對象地址和變量的地址不是指向同一個對象,天然就 false了。

JDK6


JDK7
在這裏插入圖片描述
在這裏插入圖片描述

字符串字面量是什麼時候進入字符串常量池

//代碼一
String s4 = new String("3") + new String("3");
String s6 = "33";
String s5 = s4.intern();
System.out.println(s4 == s6);
System.out.println(s5 == s6);

//代碼二
String s4 = new String("3") + new String("3");
String s6 = "33";
String s5 = s4.intern();
複製代碼

對該代碼進行編譯,以後經過 javap 命令查看其字節碼。

在這裏插入圖片描述
在這裏插入圖片描述

從字節碼文件中能夠看出 Class 文件常量池中是有「33」,可是在運行時,根據 intern()方法位置的不一樣,在代碼一中執行 String s5 = s4.intern();的時候字符串常量池中是沒有「33」對象的引用,在代碼二中執行 String s5 = s4.intern();語句在字符串常量池中發現有「33」對象的引用,區別就在於 String s6 = "33";那麼何時 Class 文件常量池中的字面量進入到字符串常量池中的呢?在上一節 Java 中方法區與常量池 中三種常量池的關聯一欄有作解釋,若有不懂,能夠前往知乎參看 new String(「字面量」) 中 「字面量」 是什麼時候進入字符串常量池的?,這位大神對此作了詳細的講解。

簡單來講:

  • HotSpot VM 的實現來講,加載類的時候,那些字符串字面量會進入到當前類的運行時常量池,不會進入全局的字符串常量池 ;在 resolve (解析)以後,纔會在堆中建立對應這些 class 文件常量池中的字符串對象實例,並在字符串常量池中駐留其引用。
  • 在字面量賦值的時候,會翻譯成字節碼 ldc 指令,ldc 指令觸發 lazy resolution 動做

到當前類的運行時常量池(runtime constant pool,HotSpot VM裏是ConstantPool + ConstantPoolCache)去查找該 index 對應的項
若是該項還沒有 resolve 則 resolve 之,並返回 resolve 後的內容。
在遇到 String 類型常量時,resolve 的過程若是發現 StringTable 已經有了內容匹配的 java.lang.String 的引用,則直接返回這個引用;
若是 StringTable 裏還沒有有內容匹配的 String 實例的引用,則會在 Java 堆裏建立一個對應內容的 String 對象,而後在 StringTable 記錄下這個引用,並返回這個引用出去。

String s=new String("xyz") 涉及到幾個對象

以前一直有個結論就是:當建立一個 string 對象的時候,去字符串常量池看是否有相應的字面量,若是沒有就建立一個。
這個說法歷來都不正確。

關於上述觀點,能夠查看 R大的回答:new一個String對象的時候,若是常量池沒有相應的字面量真的會去它那裏建立一個嗎?我表示懷疑。

回到正題,寶典上有這樣的面試題,當時記憶面試題的時候,原題是這樣「String s = new String("xyz"); 建立了幾個String Object?」,答案是兩個或一個,若是常量池中有「xyz」對象的引用,則僅建立了一個對象;反之則建立了兩個對象。

經過這段時間對 String 對象的學習,以及 JVM 內存的瞭解,回頭再看這個問題,會以爲該面試題首先提問就存在歧義,主旨不清晰,固然也就沒有合理的答案。接下來會介紹到對象的建立和類加載機制

關於對象的建立,用圖解的形式展現:


從圖中咱們能夠發現對象建立的步驟以下

  • 執行 new 指令
  • 檢查這個指令參數是否可以在常量池中定位到一個類的符號引用,而且檢查這個符號引用所表明的類是否已經被加載,解析和初始化。
  • 若是該類沒有被加載則先執行類的加載操做
  • 若是該類已經被加載,則開始給該對象在 jvm 的堆中分配內存。
  • 虛擬機初始化操做,虛擬機對分配的空間初始化爲零值。
  • 執行 init 方法,初始化對象的屬性,至此對象被建立完成。
  • Java 虛擬機棧中的 Reference 執行咱們剛剛建立的對象。

Java bytecode 代碼

0new  #2//class java/lang/String  
3: dup  
4: ldc  #3//String xyz  
6: invokespecial    #4//Method java/lang/String."<init>":(Ljava/lang/String;)V  
9: astore_1 
複製代碼

在 Java 語言裏,「new」表達式是負責建立實例的,其中會調用構造器去對實例作初始化;構造器自身的返回值類型是 void,並非「構造器返回了新建立的對象的引用」,而是 new 表達式的值是新建立的對象的引用。

對應的,在 JVM裏,「new」字節碼指令只負責把實例建立出來(包括分配空間、設定類型、全部字段設置默認值等工做),而且把指向新建立對象的引用壓到操做數棧頂。此時該引用還不能直接使用,處於未初始化狀態(uninitialized);若是某方法a含有代碼試圖經過未初始化狀態的引用來調用任何實例方法,那麼方法a會通不過JVM的字節碼校驗,從而被JVM拒絕執行。

能對未初始化狀態的引用作的惟一一種事情就是經過它調用實例構造器,在 Class 文件層面表現爲特殊初始化方法「」。實際調用的指令是 invokespecial,而在實際調用前要把須要的參數按順序壓到操做數棧上。在上面的字節碼例子中,壓參數的指令包括 dup 和 ldc 兩條,分別把隱藏參數(新建立的實例的引用,對於實例構造器來講就是「this」)與顯式聲明的第一個實際參數("xyz"常量的引用)壓到操做數棧上。在構造器返回以後,新建立的實例的引用就能夠正常使用了。

這裏又引出類加載的概念,須要注意的是,咱們日常說的加載大多不是指的類加載機制,只是類加載機制中的第一步加載。具體以下:

在代碼編譯後,就會生成 JVM(Java虛擬機)可以識別的二進制字節流文件(*.class)。而 JVM 把 Class 文件中的類描述數據從文件加載到內存,並對數據進行校驗、轉換解析、初始化,使這些數據最終成爲能夠被 JVM 直接使用的 Java 類型,這個說來簡單但實際複雜的過程叫作 JVM 的類加載機制

Class 文件中的「類」從加載到 JVM 內存中,到卸載出內存過程有七個生命週期階段。類加載機制包括了前五個階段。

以下圖所示:


其中,加載、驗證、準備、初始化、卸載的開始順序是肯定的,注意,只是按順序開始,進行與結束的順序並不必定。解析階段可能在初始化以後開始。

另外,類加載無需等到程序中「首次使用」的時候纔開始,JVM預先加載某些類也是被容許的。(類加載的時機)

在類加載階段完後後,字符串字面量會進入到字符串常量池,同時包括爲靜態變量賦程序設定的初值。關於 JVM 類加載的講解能夠參看:JVM類加載過程

String s=new String("xyz") 該行代碼運行即分爲兩個階段:類加載階段和代碼片斷自身執行的時候。因此當提問爲「String s=new String("xyz") 在運行時涉及到幾個對象」時,合理的答案是:

兩個,一個是字符串字面量"xyz"在堆中建立的對象,並將其引用駐留(intern)在全局共享的字符串常量池中,另外一個是經過new
String(String)在堆中建立並初始化的、內容與"xyz"相同的對象

」String s=new String("xyz") 在類加載時涉及到幾個對象「,該問題合理的答案就是一個。

扯點別的,若是問題改成」String s=new String("java") 在運行時涉及到幾個對象「,答案就再也不是兩個了,正確答案只有一個。詳細講解能夠參看 R大的文章:如何理解《深刻理解java虛擬機》第二版中對String.intern()方法的講解中所舉的例子?

簡單來講,就是上述代碼運行時,字符串常量池中已經有引用」java「字符串字面量,因此類加載階段沒有建立」java「對象。

String「+」符號的實現

在咱們使用中常常會用到+符號來拼接字符串,可是這個+符號在 String 中的實現仍是有講究的。若是是相加含有 String 對象,則底部是使用 StringBuilder 實現的拼接的。

經過如下的例子進行展現:

int n = 3;
String s1 = new String("3"+"3"+n);
s1.intern();
String s2 = "333";
System.out.println(s1 == s2);//true

String s3 = new String("a"+"bc");
final String s4 = "re";
String s5 = s4+"rt";
複製代碼

查看編譯後的字節碼文件:


當相加的參數有字符串變量或者其餘基礎類型變量,注意都不能是 final 修飾的,底層會使用 StringBuilder 進行拼接。若是是字符串對象直接相加,或 final 變量與字符串對象相加,在編譯階段會直接拼接在一塊兒,不須要使用 StringBuilder。

參考連接

相關文章
相關標籤/搜索