JDK8
常量池
存在於字節碼文件中
二進制字節碼中有:類基本信息、常量池、類方法定義(其中包含虛擬機指令)
常量池用於存放編譯期生成的各種字面量和符號引用
字面量:
字面量類似與我們平常說的常量,主要包括:
- 文本字符串:就是我們在代碼中能夠看到的字符串,例如String a = “aa”。其中”aa”就是字面量。
- 被final修飾的變量。
符號引用:
主要包括以下常量:
- 類或接口的全限定名:例如對於String這個類,它的全限定名就是java/lang/String。
- 字段的名稱和描述符:所謂字段就是類或者接口中聲明的變量,包括類級別變量(static)和實例級的變量。
- 方法的名稱和描述符。所謂描述符就相當於方法的參數類型+返回值類型。
反編譯javap -v Main.class
這段代碼的二進制字節碼:
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
}
}
說明:
註釋部分是javap幫我們加上的
虛擬機指令解釋執行依靠指令後邊的符號地址:#數字
,比如#2
表示去常量池找前邊標號爲#2
的那一行
所以,常量池就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
運行時常量池
運行時常量池位於方法區的元數據區中,方法區是線程共享的,因此運行時常量池也是線程共享的
運行時常量池:常量池在*.class文件,當類加載到方法區時,它的常量池信息就會放入運行時常量池,並把裏邊的符號地址變爲真實地址。應該注意的是,“文本字符串”是放在字符串常量池中的。
字符串常量池(StringTable)
字符串常量池:StringTable又稱String pool,jdk6的時候,位於方法區的運行時常量池中,jdk7及其之後,位於堆中。因爲方法區永久代的垃圾回收要等到FullGC,而FullGC的觸發不是很頻繁,導致StringTable中的對象遲遲不被回收,而Java程序中用到的字符串又很多,很容易導致永久代內存不足,所以放到了堆中。
StringTable是一個哈希表結構。堆空間是線程共享的,所有字符串常量池是線程共享的。
當類加載到方法區時,常量池中的文本字符串會加載進入字符串常量池中。
字符串常量池實際存放的堆地址,這塊堆地址空間存儲的是文本字符(這裏網上很多地方說:直接把文本字符串放在字符串常量池中,應該是不對的。此處還未找到權威證明,只在這篇文章中看到和我的想法一樣的文章:https://www.cnblogs.com/justcooooode/p/7603381.html。
歡迎指正)
下面爲了表述方便,說的是 把字符串“ab”放到字符串常量池中(或稱StringTable),實際表明的是 常量池中存放的是“ab”的引用
第一個例子
有這樣一道題:
public class Main {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
String s6 = s4.intern();
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
}
}
分析:
第一步:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
反編譯javap -v Main.class
這段代碼的二進制字節碼:
astore_1
表示把變量s1存儲到局部變量表Slot爲1的位置。
常量池中的信息如上,當常量池信息被加載到運行時常量池中時,這時候a、b、ab都是常量池中的符號,還沒有變爲java字符串。當執行到指令ldc #2
時,纔會把符號a變爲“a”字符串對象(這叫字符串的延遲加載),然後去StringTable中找,如果不存在這個字符串,就把這個字符串放入StringTable中,如果存在了,就直接使用StringTable中的這個字符串。
第二步,修改代碼如下:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
aload_1
表示去局部變量表中Slot爲1位置的數據。
astore 4
表示把變量s4存儲到局部變量表Slot爲4的位置。
執行s1 + s2
的過程實際操作是new StringBuilder().append("a").append("b").toString()
(這屬於編譯器優化)。StringBuilder中的toString()源碼如下,因此s1 + s2
就相當於new String(“ab”),重新new了一個String實例,是在堆上開闢了一塊內存存儲這個字符串,因此s3和s4不相等。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
第三步,修改代碼如下:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
從反編譯結果可以看出,"a" + "b"
是直接去StringTable中找字符串“ab”,而不是先找“a”,再找“b”,最後再拼接,可以看到s5變量的取值,和s3變量的取值是一樣的過程。因此s3和s5值相等。
第四步,修改代碼如下:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
String s6 = s4.intern();
先去局部變量表中Slot爲4位置的數據(“ab”,這個“ab”字符串時堆上的,因爲取的是s4的值),然後調用intern()方法嘗試將這個字符串放入StringTable中,如果存在,則不放入,直接拿來用,否則放入,並返回StringtTable中的這個對象(關於intern()具體知識看下邊的例子)。
最終結果:
false
true
true
第二個例子
public class Main {
public static void main(String[] args) {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
// s1和s2是final修飾的,s1+s2相當於把“ab”給s4
// 如果s1或者s2不是final修飾的,則還是用的StringBuilder
String s4 = s1 + s2;
System.out.println(s3 == s4); //true
}
}
public class Main {
public static void main(String[] args) {
String s1 = "ab";
final String s2 = "a";
String s3 = s2 + "b";
System.out.println(s1 == s2); //true
}
}
第三個例子
public class Main {
public static void main(String[] args) {
// 此時堆上有三塊空間:new String("a") new String("b") new String("ab")
// StringTable中有:["a",“b”]
String s = new String("a") + new String("b");
// 此時StringTable中還沒有"ab",所以會在常量池中創建一個"ab"
// s變爲指向常量池中的那個"ab"
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true
}
}
如果將代碼改成這樣:
public class Main {
public static void main(String[] args) {
String x = "ab";
// StringTable中有:["ab","a",“b”],下面這行代碼得到的“ab”和StringTable中的“ab”不是同一個字符串
String s = new String("a") + new String("b");
// 由於StringTable中已經存在了“ab”,因此s2直接使用這個“ab”
// s並沒有變化
String s2 = s.intern();
System.out.println(s2 == x); // true
System.out.println(s == x); // false
}
}
上邊是JDK7及其之後的情況,JDK6的時候,執行s.intern(),如果常量池中沒有和s相同值的字符串,則會重新創建一個字符串,把這個字符串放到常量池中,並沒有使用s指向的那塊空間,所以常量池中的"ab"和s並不相等。
在JDK6中執行:
public class Main {
public static void main(String[] args) {
String s = new String("a") + new String("b");
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // false
}
}