JVM內存結構之淺析常量池、運行時常量池、StringTable

介紹前先放一張JVM的內存結構1.6和1.8版本的各組成部分存放位置的模型圖

 

注:以下的分析都是以jdk1.8版本爲例

 

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

1、常量池就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息,我們可以通過Javap -v  類名.class 指令反編譯一個簡單的程序看到如下的常量池信息

2、運行時常量池:常量池是 class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,並把裏面的符號地址變爲真實地址

3、字符串池:在JVM裏實現字符串池功能的是一個StringTable類,它的底層是一個HashTable,裏面存的是字符串對象的引用而不是字符串實例本身),真正的字符串實例是存放在堆內存中的並且字符串池在邏輯上是屬於運行時常量池的一部分

二、常量池和字符串池的關係(重點

1、首先看一段代碼

 public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String str = new String("a");

        System.out.println(s1 == str);  //false
}

通過反編譯可看到如下信息:

因此說明:最開始編譯時字符串都是常量池中的符號,尚未轉化爲對象,當程序執行時,常量池中的信息都會被加載到運行時常量池中,這才轉化成了對象,並且看StringTable中有沒有"a","b"對象,如果沒有則把 "a" 和 "b" 對象的引用值存入StringTable,真正的對象實例則在堆中;如果有的話則不會存入,這樣就避免了重複創建字符串對象

2、然後再來分析String str = new String("a"),這行代碼創建的對象個數因StringTable中有沒有“a”對象而異,如果字符串池有“a”,則此時只會創建一個對象:也就是new的一個字符串對象,存放在堆中;如果沒有就會創建兩個對象,一個是new的對象存放在堆中,一個是“a”字符串常量對象,存放在StringTable中

3、知道上面的原理後,System.out.println(s1 == str)爲false就不足爲奇了,==比較的是地址,一個在串池中,一個在堆中,地址自然而然不同。

三、StringTable特性

1、能避免創建重複的字符串對象

這個上面就說了,此處就不重複

2、字符串變量拼接原理(StringBuilder)

首先看一段代碼

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;

        System.out.println(s3 == s4);   //false
    }

然後通過反編譯,我們可以看到如下圖的信息,字符串變量的拼接是通過StringBuilder對象來實現的 (這也是爲什麼平時進行字符串變量拼接的時候推薦使用StringBuilder而不是String)

 然後再來看看toString方法的源碼,看到最後又new了一個String對象

可知:在進行字符串變量拼接的時候,是利用StringBuilder來實現的,並且最後new了一個新的String對象放入堆中, 所以上面就爲false了,地址一個在串池中,一個在堆中,肯定不一樣。

3、字符串常量拼接原理(編譯器優化)

上代碼

 public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";

        String s5 = "a" + "b";

        System.out.println(s3 == s5);  //true
    }

然後通過反編譯可以看到如下信息,由於"a","b"都是字符串常量,所以在進行字符串拼接的時候,編譯器自動優化,變成了"ab"

所以在進行字符串常量拼接時,編譯器優化直接確定了"ab"對象,而此時StringTable已經有"ab" 的引用,所以直接返回引用值,不需要重新重新創建對象,因此結果就爲true了。

4、jdk1.8和jdk1.6中intern()方法的運用

  • 1.8 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有則將該字符串的引用放入串池
  • 1.6 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象複製一份,放入串池

1)jdk1.8中intern方法的運用 

 public static void main(String[] args) {

        String s1 = new String("a")+new String("b");
        String s2 = s1.intern();

        System.out.println(s2 == "ab"); //true
        System.out.println(s1 == "ab"); //true
        System.out.println(s2 == s1);  //true

    }

分析

第一步:new String("a")時將"a"放入串池,new String("b")時將"b"放入串池,最後拼接的時候,返回的是new String("ab"),因爲此時的"ab"是利用StringBuilder拼接而成,並不是字面量,所以"ab"不會放入串池中,而是在堆中

第二部:使用intern方法 可以將字符串對象存放進StringTable中,如果原來的StringTable中沒有此字符串對象,則將此字符串對象的引用放入StringTable中;如果有,則直接返回該字符串對象的引用

對於上訴代碼可知:執行完了String s2 = s1.intern()代碼後,此時“ab”對象的引用就存放進StringTable中,並將s1,s2都指向了"ab"的引用,所以三個都爲true

繼續看如下代碼

public static void main(String[] args) {

 
        String str = "ab";

        String s1 = new String("a")+new String("b");

        String s2 = s1.intern();

        System.out.println(s2 == str); //true
        System.out.println(s1 == str); //false

        System.out.println(s2 == s1);  //false

   
    }

分析:最開始執行完String str = "ab"後,就將"ab"存放進StringTable中,所以執行String s2 = s1.intern()後,並不會將s1的引用放入StringTable中,而是直接將原來串池中"ab"的引用返回給s2,因此s1是在堆中,s2在StringTable中,自然就不同了。

2)jdk1.6中intern方法的運用

 public static void main(String[] args) {

        String s1 = new String("a")+new String("b");
        String s2 = s1.intern();

        System.out.println(s2 == "ab"); //true
        System.out.println(s1 == "ab"); //false
        System.out.println(s2 == s1);  //false

    }

同樣的代碼但在jdk1.6中是不同的

分析:之前的分析都是一樣的,唯一的不同點是在執行完了String s2 = s1.intern()代碼後,此時是將s1拷貝一份,存放進StringTable中,原來的s1還是在堆中,所以 s1 == s2 自然就爲false了

 

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