常量池、運行時常量池、字符串常量池

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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章