StringTable
- 1.StringTable的特性
- 1.1.面試題
- 1.2.常量池與串池的關系
- 1.3.字串的拼接
- 1.4.編譯器優化
- 1.5.intern方法
- 1.5.1.intern方法(1.8)
- 1.5.2.intern方法(1.6)
- 1.6.StringTable的特性總結
- 2.StringTable位置
- 3.StringTable垃圾回收
- 4.StringTable性能調優
1.StringTable的特性
1.1.面試題
先看幾道面試題:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 問
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd"; x2.intern();
// 問,如果調換了【最后兩行代碼】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
以上代碼的運行結果如何,如果尚有疑惑,請看下面的分析
1.2.常量池與串池的關系
public static void main(String[] args) {
String s1 = "a"; // 懶惰的
String s2 = "b";
String s3 = "ab";
}
反編譯Demo1.class(之前需要運行將.java檔案生成.class檔案)

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\Demo1.class
常量池中資訊
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // Demo1
#6 = Class #29 // java/lang/Object
......
main方法位元組碼資訊
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 6
line 14: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
可以看到ldc #2,也就是到常量池中2號位置加載資訊,這個例子中2號位置對應字串物件String a(注釋中),然后astore_1也就是把加載好的a字串物件存入1號的區域變數,低下的 LocalVariableTable:也就是main方法堆疊幀運行時區域變數表中的變數,編號是1.同理,ldc #3,String b存入到區域變數表中2號位置去,ab字串存入到3中去,
看完上面代碼之后,我們要理清楚常量池與串池的關系,
編譯后常量池中的資訊,都會被加載到運行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變為 java 字串物件
當具體執行到參考它的代碼上,就會變成java字串物件
ldc #2 會把 a 符號變為 “a” 字串物件
在變為a字串物件之后,還要準備好一塊空間,即StringTable字串常量池,剛開始里面是空的,將a變為字串物件之后,就會去StringTable中找,看有沒有相同的key,在資料結構上是一個hash表,長度固定,不能擴容,如果沒有,就會把a放入串池,
只有執行到用到它的代碼,才開始創建字串物件,放入到常量池中
它們在行為上是懶惰的
如果串池中有了,就會使用串池中的物件
ldc #3 會把 b 符號變為 “b” 字串物件
ldc #4 會把 ab 符號變為 “ab” 字串物件
最終
StringTable [ “a”, “b” ,“ab” ] hashtable 結構,不能擴容
1.3.字串的拼接
public static void main(String[] args) {
String s1 = "a"; // 懶惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
}
將上述代碼反編譯,生成反編譯結果,對于String s4 = s1 + s2;部分的代碼如下:
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
它先創建了StringBuilder()物件,然后呼叫了init()構造方法,aload_1把s1這個引數加載進來,即從區域變數表中拿到s1引數a,與astore1相反,接下來呼叫了append方法,s1作為引數,aload2又把s2拿到了,即字串b,作為append方法,最后使用了一個toString方法,最后astore_4,即把toString轉換后的結果存入到4號的區域變數中,
我們可以看看StringBuilder的toString原碼
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
即創建了一個新的值為ab的物件,存入到s4這個物件中,
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
System.out.println(s3==s4);
結果是false
雖然它們值是相同的,但是s3是在串池中,而s4是new出來的字串物件,是在堆里面的,它們是兩個物件,所以列印false
1.4.編譯器優化
public static void main(String[] args) {
String s1 = "a"; // 懶惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在編譯期間的優化,結果已經在編譯期確定為ab
}
反編譯
生成的最后一行代碼的位元組碼如下:
29: ldc #4 // String ab
31: astore 5
我們可以看到,它不是先找a再找b最后拼接到一起,而是直接找到的就是直接拼接好的ab這個符號,并且存入到區域變數里面,下標為5的位置,
而這是String s3 = “ab”;的位元組碼
6: ldc #4 // String ab
8: astore_3
在開始的時候,串池沒有ab這個物件,s3創建出來了,就放入到串池,而s5再次參考ab這個字串時候,就先會去字串常量池中找,找到了,然后就會直接用串池中字串物件,所以存盤s3的物件和存盤s5的物件,都是串池中的字串物件,
最后發現下面結果輸出為真,
String s3 = "ab";
String s5 = "a" + "b"; // javac 在編譯期間的優化,結果已經在編譯期確定為ab
System.out.println(s3 == s5);
結果為true
javac 在編譯期間的優化,結果已經在編譯期確定為ab
而上面拼接的是變數,既然是變數,說明以后參考的時候可能會發生修改,所以值不能確定,所以在運行期間用stringbuilder的方式來拼接,
1.5.intern方法
1.5.1.intern方法(1.8)
呼叫字串物件的intern方法,會將該字串物件嘗試放入到串池中
- 如果串池中沒有該字串物件,則放入成功
- 如果有該字串物件,則放入失敗
無論放入是否成功,都會回傳串池中的字串物件
注意:此時如果呼叫intern方法成功,堆記憶體與串池中的字串物件是同一個物件;如果失敗,則不是同一個物件
我們有以下代碼
String s=new String("a")+new String("b");
經過前面學習,我們知道了
常量a,b都被放入到了常量池中
new出來的兩個物件String(“a”)和String(“b”)被放入到了堆中,他們值雖然相同,但是物件不同,
s又參考了物件,new String(“ab”),它只存在于堆中,不存在串池中,
即常量池中存在[“a”,“b”]
而堆中存在new String(“a”),new String(“b”),new String(“ab”)
我們能不能把ab存入到常量池中呢,可以通過intern方法
intern方法將這個字串物件嘗試放入串池,如果有則不會放入,如果沒有則放入串池,會把串池中的物件回傳,
public class Main {
public static void main(String[] args) {
//"a" "b" 被放入串池中,str則存在于堆記憶體之中
String str = new String("a") + new String("b");
//呼叫str的intern方法,這時串池中沒有"ab",則會將該字串物件放入到串池中,此時堆記憶體與串池中的"ab"是同一個物件
String st2 = str.intern();
//給str3賦值,因為此時串池中已有"ab",則直接將串池中的內容回傳
String str3 = "ab";
//因為堆記憶體與串池中的"ab"是同一個物件,所以以下兩條陳述句列印的都為true
System.out.println(str == st2);
System.out.println(str == str3);
}
}
回傳結果為true,true
如果將在最開始定義了String x="ab"呢
public class Main {
public static void main(String[] args) {
//此處創建字串物件"ab",因為串池中還沒有"ab",所以將其放入串池中
String str3 = "ab";
//"a" "b" 被放入串池中,str則存在于堆記憶體之中
String str = new String("a") + new String("b");
//此時因為在創建str3時,"ab"已存在與串池中,所以放入失敗,但是會回傳串池中的"ab"
String str2 = str.intern();
//false
System.out.println(str == str2);
//false
System.out.println(str == str3);
//true
System.out.println(str2 == str3);
}
}
1.5.2.intern方法(1.6)
呼叫字串物件的intern方法,會將該字串物件嘗試放入到串池中
如果串池中沒有該字串物件,會將該字串物件復制一份,再放入到串池中
如果有該字串物件,則放入失敗
無論放入是否成功,都會回傳串池中的字串物件
注意:此時無論呼叫intern方法成功與否,串池中的字串物件和堆記憶體中的字串物件都不是同一個物件
1.6.StringTable的特性總結
常量池中的字串僅是符號,只有在被用到時才會轉化為物件
利用串池的機制,來避免重復創建字串物件
字串變數拼接的原理是StringBuilder
字串常量拼接的原理是編譯器優化
可以使用intern方法,主動將串池中還沒有的字串物件放入串池中
注意:無論是串池還是堆里面的字串,都是物件
用來放字串物件且里面的元素不重復
面試結果如下:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 問
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";
// 問,如果調換了【最后兩行代碼】的位置呢
System.out.println(x1 == x2); //調換前true,調換后fals
}
此時所有的問題是不是都迎刃而解了
2.StringTable位置

在1.6中是常量池的一部分,隨常量池存盤在永久代中,1.7開始之后就放入到了堆中,因為永久代的記憶體回收效率很低,永久代很難觸發垃圾回收,需要等到老年代空間不足才會回收,觸發效率不高,Springtable是非常常用的,放入到常量池中容易導致永久代記憶體不足,而堆里面Springtable觸發垃圾回收的情況比較簡單,
3.StringTable垃圾回收
StringTable在記憶體緊張時,會發生垃圾回收,
4.StringTable性能調優
SpringTable底層是哈希表,哈希表性能與其大小有關,如果哈希表桶的個數比較多,元素分散,哈希碰撞的幾率就比較小,查找效率較高,
因為StringTable是由HashTable實作的,所以可以適當增加HashTable桶的個數,來減少字串放入串池所需要的時間
-XX:StringTableSize=xxxx
考慮是否需要將字串物件入池
可以通過intern方法減少重復入池,相同的地址intern之后,在記憶體中只會存盤一份,這樣就能減少字串對記憶體占用,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/255194.html
標籤:java
上一篇:大數加法求解思路
下一篇:Java核心基礎
