探索JVM的底層祕密(一)——如何正確理解Java的常數池

2020-10-22 11:00:29

如果你點開了本篇文章,那麼恭喜你發現寶藏了!

博主接下來將會更新整個系列的 《探索JVM的底層祕密》 文章,為大家完整的剖析JVM的底層原理。

作者最近在優化JVM記憶體模型這方面的內容,發現自己對於Java中的常數池的理解有點零碎,做個總結,於是就有了這篇文章。本篇文章所有知識點基於jdk8。
jdk6、jdk7不適用,如果有疑問,歡迎在評論區留言。廢話不多說,直接上程式碼。

比如你寫了一段這樣的Java程式碼,JVM是如何處理的呢?

1、Java程式碼

public class StringTest2 {
    String name = "子牙";
    
    public static void main(String[] args) {
        StringTest2 obj = new StringTest2();
    }
}

2、class檔案之常數池(圖1)

在這裡插入圖片描述

3、預設構造方法位元組碼(圖2)

在這裡插入圖片描述



常數池分類

1、class檔案中的常數池

這個常數池中主要存放兩大類常數:字面量、符號參照。

字面量即文字字串,如index=10的Code、index=11的LineNumberTable……還有宣告為final的常數。

符號參照則屬於編譯原理方面的概念,包含三類:

  • 類和介面的全限定名,如index=4存放的是CONSTANT_Class_info結構,指向的是類的全限定名
  • 欄位的名稱和描述符,如index=3存放的是CONSTANT_Fieldref_info結構,指向的是欄位的名稱與描述符
  • 方法的名稱和描述符,如index=17存放的是CONSTANT_Methodref_info結構,指向的是方法的名稱與描述符

2、執行時常數池

方法區的一部分。我們常說的常數池,就是指這一塊區域:方法區中的執行時常數池。

那資料是何時存入這塊區域的呢?是在類載入階段,類載入器子系統會將class檔案中的常數池中的資料封裝成相應的CONSTANT_*結構存入進去。

這裡重點說下index=2的資料項。index=2對應的資料結構是CONSTANT_String,但是在類載入階段,index=2儲存的資料結構卻是JVM_CONSTANT_UnresolvedString,為什麼會這樣呢?因為載入類的時候,還沒有解析字串字面量,即沒有將符號參照轉為直接參照。那何時解析的呢?執行引擎執行ldc指令的時候。不懂?往後看。

3、全域性字串常數池

這個常數池在JVM層面就是一個StringTable,只儲存對java.lang.String範例的參照,而不儲存String物件的內容。

一般我們說一個字串進入了字串常數池其實是說在這個StringTable中儲存了對它的參照,反之,如果說沒有在其中就是說StringTable中沒有對它的參照。

解析上面的程式碼

基礎知識講完了,咱們來實戰一下。就以JVM處理上面貼出的程式碼為例,給童鞋們分享一下執行流程:

1、呼叫javac命令編譯java檔案生成class檔案,class檔案中的常數池(圖1)

2、呼叫java命令開始執行這個class檔案,類載入器子系統將class檔案載入進記憶體,並將常數池中的資料封裝成相應的CONSTANT_*結構存入執行時常數池。這時候常數池中index=2的位置存放的是JVM_CONSTANT_UnresolvedString而不是JVM_CONSTANT_String_info

3、執行引擎執行StringTest2的預設建構函式,即圖3。大家是否注意到ldc指令,那這個指令做了什麼呢?執行引擎執行ldc指令時,會根據ldc後面的運算元去執行時常數池中查詢對應的值,並判斷是否已完成解析,如果已解析就直接返回字串在堆中的參照,即記憶體地址。如果沒有解析進去解析,那如何解析呢?

4、根據JVM_CONSTANT_UnresolvedString中存放的index去執行時常數池中查詢CONSTANT_Utf8_info結構,這個結構存放了字串的具體內容及字串長度。然後判斷字串常數池中是否有這個字串的參照,如果有就直接返回,如果沒有就去堆中建立一個對應內容的String物件,並將參照儲存在字串常數池中。這樣就完成了String型別的解析工作。

intern方法做了什麼

如果當前字串內容存在於字串常數池中,即使用 equas() 方法返回ture,那直接返回此字串在常數池的參照。如果不在字串常數池中,那麼在常數池建立一個參照並且指向堆中已存在的字串,然後返回常數池中的地址。是不是有點抽象,對著面試題再看一遍。

注意:該方法是有返回值的,返回的是常數池中的地址。為什麼要強調呢?看面試題。

==與equals

==比較的是參照,即記憶體地址。equals比較的是兩個物件的內容。

字串相關面試題剖析

如果你對Java中的常數池理解得不是很透徹,這道面試題你還真不一定能答上來。就算告訴了你答案你可能也會一臉懵逼。那這篇文章你已經看到這裡了,我希望你已明瞭。建議同學們先不要看答案以及我的解析,先自己回答一下,然後給出自己的分析,再看答案。

1、上程式碼


public class StringTest1 {
    public static void main(String[] args) {
        String s1 = "子牙真帥";
        String s2 = "子牙真帥";

        String a = "子牙";

        String s3 = new String(a + "真帥");
        String s4 = new String(a + "真帥");

        System.out.println("s1 == s2: " + (s1 == s2));
        System.out.println("s2 == s3: " + (s2 == s3));
        System.out.println("s3 == s4: " + (s3 == s4));

        s3.intern();
        System.out.println("s2 == s3: " + (s2 == s3));

        s3 = s3.intern();
        System.out.println("s2 == s3: " + (s2 == s3));
    }
}

2、返回結果

s1 == s2: true
s2 == s3: false
s3 == s4: false
s2 == s3: false
s2 == s3: true

3、解析

  • 【s1 == s2: true】:因為s1、s2都指向字串常數池中同一字串:hello
  • 【s2 == s3: false】:因為s2是指向字串常數池中的參照,s3是指向堆中的參照,自然不相等
  • 【s3 == s4: false】:因為s3、s4是兩個不同的物件,自然不相等
  • 【s2 == s3: false】:因為s3雖然呼叫了intern方法,但是未處理返回值,所以s3依然是指向堆中的參照
  • 【s2 == s3: true】:因為s3呼叫了intern方法,並且返回給了s3,此時 s2、s3 都直接指向常數池的同一個字串。

好了,今天的文章就暫時先寫到這裡了,如果本篇文章對你有幫助,想要繼續瞭解之後的更多JVM底層知識,請一定要點贊+關注,一鍵三連!