簡介
字符陣列的存盤方式
字串常量池
字串在java程式中被大量使用,為了避免每次都創建相同的字串物件及記憶體分配,JVM內部對字串物件的創建做了一定的優化,在Permanent Generation中專門有一塊區域用來存盤字串常量池(一組指標指向Heap中的String物件的記憶體地址),
在HotSpot VM里實作的string pool功能的是一個StringTable類,它是一個HashTable,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實體只有一份,被所有的類共享,字串常量由一個一個字符組成,放在了StringTable上,在JDK6.0中,StringTable的長度是固定的,長度就是1009,因此如果放入String Pool中的String非常多,就會造成hash沖突,導致鏈表過長,當呼叫String#intern()時會需要到鏈表上一個一個找,從而導致性能大幅度下降;
- 在JDK6.0及之前版本中,String Pool里放的都是字串常量;
- 在JDK7.0中,由于String#intern()發生了改變,因此String Pool中也可以存放放于堆內的字串物件的參考,關于String在記憶體中的存盤和String#intern()方法的說明,
字串Hashcode
不通方式創建字串在JVM存盤的形式
-
雙引號方式
雙引號引起來的字串,首先從常量池中查找是否存在此字串,如果不存在,則在常量池中添加此字串,在堆中創建字串物件,因String底層是通過char陣列形式存盤的,所以同時會在堆中生成一個TypeArrayOopDesc用來存盤char陣列物件,如果存在,則直接參考此字串物件,

測驗代碼1:
public static void test1(){ String s1="11"; String s2="11"; System.out.println(s1==s2); }
測驗結果:

原因分析:
s1代碼執行后,常量池中添加了“11”這個常量,在堆中也創建了String物件并參考此常量的,當s2代碼執行時,先在常量池中查找是否存在“11”這個常量,發現常量池中存在這個值,就找到參考此常量的字串物件,將s2的參考指向找到的字串物件,因為s1和s2指向同一個地址,所以比較結果為true,

-
new String
1、首先從常量池中查找是否存在括號內的常量,如果不存在,則在常量池中添加此字串,在堆中創建字串物件,因String底層是通過char陣列形式存盤的,所以同時會在堆中生成一個TypeArrayOopDesc用來存盤char陣列物件,如果存在,則直接參考堆中存在的字串物件,
2、通過new方式創建的String物件,每次都會在Heap上創建一個新的實體,并將此新實體中char陣列物件,指向第一步堆中的已經存在的TypeArrayOopDesc,
測驗代碼:
public static void test2() { String s1 = new String("11"); String s2 = new String("11"); System.out.println(s1 == s2); }
測驗結果:

原因分析:
通過new方式創建的String物件,每次都會在Heap上創建一個新的實體,所以s1和s2的分別指向了不同的實體,參考地址不同,

測驗代碼:
public static void test3() { String s1 = new String("11"); String s2 = "11"; System.out.println(s1 == s2); }
測驗結果:

原因分析:
當執行s1時,首先會將括號內的字面量常量“11”添加到常量池中,并且在堆中生成字串實體及char陣列實體TypeArrayOopDesc,再通過new方式創建的String物件,會在Heap上新創建一個實體,此新實體中char陣列不需要新的實體,指向堆中的已存在的TypeArrayOopDesc,
當執行s2時,在常量池中發現常量已存在,則直接將虛擬機堆疊的指向堆中代表此常量的字串實體,
因此s1和s2的分別指向了不同的實體,參考地址不同,
【缺圖】
字串在JVM中是如何拼接的
測驗代碼:
public static void test4(){ String s2="1"+"1"; String s1="11"; System.out.println(s1==s2); }
測驗結果:

原因分析:
檔案在編譯期成位元組碼時,編譯器將“1”+“1”變成了“11”,編譯后,相當于s2="11",就與上面的測驗代碼1相同了,具體原因見測驗代碼1的原因分析,

測驗代碼:
public static void test5(){ String s1="1"; String s2="1"; String s3=s1+s2; String s4="11"; System.out.println(s3==s4); }
測驗結果:

原因分析:
編譯器在編譯時無法確定s3的值,是在運行時才能確定,保存在jvm的堆里面,在拼接的時候,先在常量池里面生成是s1、s2的字串,在執行加號的時候,會從常量池中取出s1、s2常量,在堆中生成兩個字串物件,然后再生成第三個字串物件來保存兩個物件拼接后的值,

測驗代碼:
public static void test6() { final String s1 = "1"; final String s2 = "1"; String s3 = s1 + s2; String s4 = "11"; System.out.println(s3 == s4); }
測驗結果:

原因分析:
通過s1、s2增加final修飾符,s1和s2的值賦值后不允許改變,這樣編譯器在編譯時會把s3編譯成s3="11",所以在執行時會字串常量池中添加“11”這個常量,執行s4時會在常量池中找到“11”這個常量, s4會執行堆中已存在的字串物件,因此s3和s4相等,

intern做了什么
intern()方法:
public String intern()
JDK源代碼如下圖:

回傳字串物件的規范化表示形式,
一個初始時為空的字串池,它由類 String 私有地維護,
當呼叫 intern 方法時,如果池已經包含一個等于此 String 物件的字串(該物件由 equals(Object) 方法確定),
則回傳池中的字串,否則,將此 String 物件添加到池中,并且回傳此 String 物件的參考,
它遵循對于任何兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true,
所有字面值字串和字串賦值運算式都是內部的,
回傳:
一個字串,內容與此字串相同,但它保證來自字串池中,
盡管在輸出中呼叫intern方法并沒有什么效果,但是實際上后臺這個方法會做一系列的動作和操作,
在呼叫”ab”.intern()方法的時候會回傳”ab”,但是這個方法會首先檢查字串池中是否有”ab”這個字串,
如果存在則回傳這個字串的參考,否則就將這個字串添加到字串池中,然會回傳這個字串的參考,
測驗代碼:
public static void test8_3(){ String s1="11"; String s2=new String("11"); String s3=s2.intern(); System.out.println(s1==s2);//#1 System.out.println(s1==s3);//#2 }
測驗結果:

原因分析:
結果 #1:因為s1指向的是字串中的常量,s2是在堆中生成的物件,所以s1==s2回傳false,
結果 #2:s2呼叫intern方法,會將s2中值(“string”)復制到常量池中,但是常量池中已經存在該字串(即s1指向的字串),
所以直接回傳該字串的參考,因此s1==s2回傳true,
測驗代碼:
public static void test8_4(){ String s1="1"; final String s2="1"; String s3="11"; String s4="1"+"1"; String s5=s1+"1"; String s6=s2+"1"; String s7=new String("11").toString().intern(); System.out.println(s3==s4);//#1 System.out.println(s3==s5);//#2 System.out.println(s3==s6);//#3 System.out.println(s3==s7);//#4 }
測驗結果:
原因分析:
通過反編譯檔案,比較容易理解:

在解釋上述執行程序之前,先了解兩條指令:
ldc:Push item from run-time constant pool,從常量池中加載指定項的參考到堆疊,
astore_<n>:Store reference into local variable,將參考賦值給第n個區域變數,
現在我們開始解釋位元組碼的執行程序:
0: ldc #8 :加載常量池中的第八項(“1”)到堆疊中,
2: astore_0 :將1中的參考復制給第零個區域變數,即 String s1="1";
3: ldc #8 :加載常量池中的第八項(“1”)到堆疊中,
5: astore_1 :將3中的參考賦值給第一個區域變數,即 final String s2="1";
6: ldc #3 :加載常量池中的第三項(“11”)到堆疊中,
8: astore_2 :將6中的參考賦值給第二個區域變數,即 String s3="11";
9: ldc #3 :加載常量池中的第三項(“11”)到堆疊中,
11: astore_3 :將9中的參考賦值給第三個區域變數,即 String s4="11";
結果#1:s3==s4 肯定會回傳true,因為s3和s4都指向常量池中的同一參考地址,
其實在JAVA 1.6之后,常量字串的“+”操作,編譯階段直接會合成為一個字串,
12: new #9:生成StringBuilder的實體,
15: dup :賦值12生成物件的參考并壓入堆疊中,
16: invokespecial #10: 滴啊用常量池中的第十項,即StringBuilder.<init>方法,
以上三條指令的作用是生成一個StringBuilder的物件,
19: aload_0 :加載第零個區域變數的值,即“1”
20: invokevirtual #11 : 呼叫StringBuilder物件的append方法,
23: ldc #8 :加載常量池中第八項(“1”)到堆疊中,
25: invokevirtual #11 :呼叫StringBuilder物件的append方法,
28: invokevirtual #12 :呼叫StringBuilder物件的toString方法,
31: astore 4 :將28中的結果參考賦值給第四個區域變數,即對變數s5進行賦值,
結果 #2:因為s5實際上是stringBuilder.append()生成的結果,所以與s3不相等,結果回傳false,
33: ldc #3:加載常量池中第三項(“11”) 到堆疊中,
35: astore 5 :將33中的參考賦值給第五個區域變數,即s6=“11”,
結果 #3 :因為s3和s6指向的都是常量池中相同的參考,所以s3==s6回傳true,
這里我們還能發現一個現象,對于加了final屬性的欄位,編譯期直接進行了常量替換,而對于非final欄位則是在運行期進行賦值處理的,
37: new #6 :創建String物件,
40: dup :復制參考并壓如堆疊中,
41: ldc #3:加載常量池中的第三項(“11”)到堆疊中,
43: invokespecial #7 :呼叫String.”<init>”方法,并傳42步驟中的參考作為引數傳入該方法,
46: invokevirtual #20 :呼叫String.tostring()方法,
49: invokevirtual #13 :呼叫String.intern方法,
從37到49的對應的原始碼就是new String("11").toString().intern();
52: astore 6 :將49步回傳的結果賦值給變數6,即s7指向11在常量池中的位置,
結果 #6 :因為s7和str3都指向的都是常量池中的同一個字串,所以s3==s7回傳true,
測驗代碼:
public static void test8_5_1(){ String s1=new String("1")+new String("1"); s1.intern(); String s2="11"; System.out.println(s1==s2);//#1 } public static void test8_5_2(){ String s2="11"; String s1=new String("1")+new String("1"); s1.intern(); System.out.println(s1==s2);//#2 }
測驗結果:

原因分析:
JDK 1.7后,對于第一種情況回傳true,但是調換了一下位置回傳的結果就變成了false,這個原因主要是從JDK 1.7后,
HotSpot 將常量池從永久代移到了元空間,正因為如此,JDK 1.7 后的intern方法在實作上發生了比較大的改變,
JDK 1.7后,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則回傳常量池中的參考,這一點與之前沒有區別,區別在于,
如果在常量池找不到對應的字串,則不會再將字串拷貝到常量池,而只是在常量池中生成一個對原字串的參考,所以:
結果 #1:在第一種情況下,因為常量池中沒有“11”這個字串,所以會在常量池中生成一個對堆中的“11”的參考,
而在進行字面量賦值的時候,常量池中已經存在,所以直接回傳該參考即可,因此s1和s2都指向堆中的字串,回傳true,
結果 #2:調換位置以后,因為在進行字面量賦值(String s2 = “11″)的時候,常量池中不存在,所以s2指向的常量池中的位置,
而s1指向的是堆中的物件,再進行intern方法時,對s1和s2已經沒有影響了,所以回傳false,
測驗代碼:
public static void test8_6_1(){ String s1=new StringBuilder("1").append("1").toString(); System.out.println(s1==s1.intern());//#1 } public static void test8_6_2(){ String s1=new StringBuilder("11").toString(); System.out.println(s1==s1.intern());//#2 }
測驗結果:

原因分析:
結果#1 :
String s1 = new StringBuilder("1").append("1").toString();
System.out.println(s1==s1.intern());
上面的代碼等價于下面的代碼
String a = "1";
String b = "1";
String str3 = new StringBuilder(a).append(b).toString();
System.out.println(s1==s1.intern());
很容易分析出:
“11” 最先創建在堆中 s1.intern()然后快取在字串常連池中 運行結果為true.
結果#2:
String s1 = new StringBuilder("11").toString();
System.out.println(s1==s1.intern());
可以寫成下面的形式
String a = "11";
String s1 = new StringBuilder(a).toString();
System.out.println(s1==s1.intern());
很容易分析出:
“11” 最先創建在常量池中, 運行結果為false.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/69390.html
標籤:Java
