JVM筆記:Java虛擬機(jī)的常量池

這篇文章主要是做一個(gè)總結(jié),將查找到相關(guān)的資料自己做一個(gè)整理,最后會(huì)列出查找過(guò)的相關(guān)資料,感興趣的可以去翻一翻。

常量池

  • class文件常量池(class constant pool)

    常量池可以理解為Class文件之中的資源倉(cāng)庫(kù),它是Class文件結(jié)構(gòu)中與其他項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類型,包含了類也是占用Class文件中第一個(gè)出現(xiàn)的表類型數(shù)據(jù)項(xiàng)目。

    常量池中主要存放兩大類常量:字面量(Literal)和符號(hào)引用(Symbolic References)。字面量比較接近于Java語(yǔ)言層面的常量概念,如文本字符串、聲明為final的常量值等。而符號(hào)引用則屬于編譯原理方面的概念,包含了下面三類常量:

    • 類和接口的全限定名(Full Qualified Name)
    • 字段的名稱和描述符(Descriptor)
    • 方法的名稱和描述符

    類和接口的全限定名,例如:com/example/demo/Demo.class

    字段的名稱和描述符,例如:Field a:[Ljava/lang/String

    方法的名稱和描述符,例如:Method java/lang/String."<init>":(Ljava/lang/String;)V

    后兩個(gè)是字節(jié)碼指令,不懂得可以查閱下相關(guān)資料(TODO)

    可以通過(guò)查看字節(jié)碼的形式來(lái)查看Class的常量池的內(nèi)容,因?yàn)槭窃诰幾g時(shí)產(chǎn)生的,也可以稱為靜態(tài)常量池。

public class Main {
   private int a=1;
   private int b=1;
   private Aload c=new Aload();
   private String [] d =new String[10];
   public static void main(String[] args) {

   }
}
字節(jié)碼:
public class com.verzqli.snake.Main
 minor version: 0
 major version: 51
 flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //這里就是class文件的常量池
  #1 = Methodref          #10.#30        // java/lang/Object."<init>":()V
  #2 = Fieldref           #9.#31         // com/verzqli/snake/Main.a:I
  #3 = Fieldref           #9.#32         // com/verzqli/snake/Main.b:I
  #4 = Class              #33            // com/verzqli/snake/Aload
  #5 = Methodref          #4.#30         // com/verzqli/snake/Aload."<init>":()V
  #6 = Fieldref           #9.#34         // com/verzqli/snake/Main.c:Lcom/verzqli/snake/Aload;
  #7 = Class              #35            // java/lang/String
  #8 = Fieldref           #9.#36         // com/verzqli/snake/Main.d:[Ljava/lang/String;
  #9 = Class              #37            // com/verzqli/snake/Main
 #10 = Class              #38            // java/lang/Object
 #11 = Utf8               a
 #12 = Utf8               I
 #13 = Utf8               b
 #14 = Utf8               c
 #15 = Utf8               Lcom/verzqli/snake/Aload;
 #16 = Utf8               d
 #17 = Utf8               [Ljava/lang/String;
 #18 = Utf8               <init>
 #19 = Utf8               ()V
 #20 = Utf8               Code
 #21 = Utf8               LineNumberTable
 #22 = Utf8               LocalVariableTable
 #23 = Utf8               this
 #24 = Utf8               Lcom/verzqli/snake/Main;
 #25 = Utf8               main
 #26 = Utf8               ([Ljava/lang/String;)V
 #27 = Utf8               args
 #28 = Utf8               SourceFile
 #29 = Utf8               Main.java
 #30 = NameAndType        #18:#19        // "<init>":()V
 #31 = NameAndType        #11:#12        // a:I
 #32 = NameAndType        #13:#12        // b:I
 #33 = Utf8               com/verzqli/snake/Aload
 #34 = NameAndType        #14:#15        // c:Lcom/verzqli/snake/Aload;
 #35 = Utf8               java/lang/String
 #36 = NameAndType        #16:#17        // d:[Ljava/lang/String;
 #37 = Utf8               com/verzqli/snake/Main
 #38 = Utf8               java/lang/Object
  • 運(yùn)行時(shí)常量池

    當(dāng)java文件被編譯成class文件之后,就會(huì)生成上面的常量池,在Class文件中描述的各種信息,最終都需要加載到虛擬機(jī)中之后才能運(yùn)行和使用。
    類從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存位置,他的生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initalization)、使用(Using)和卸載(Unloading),其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱Wie連接(Linking)。

    而當(dāng)類加載到內(nèi)存中后,JVM就會(huì)將Class常量池中的內(nèi)容存放到運(yùn)行時(shí)常量池中,由此可知,運(yùn)行時(shí)常量池也是每個(gè)類都有一個(gè)。在解析過(guò)程中需要將常量池中所有的符號(hào)引用(classes、interfaces、fields、methods referenced in the constant pool)轉(zhuǎn)為直接引用(得到類或者字段、方法在內(nèi)存中的指針或者偏移量,以便直接調(diào)用該方法)。直接引用可以是內(nèi)存中,直接指向目標(biāo)的指、相對(duì)偏移量,或是一個(gè)能間接定位到目標(biāo)的句柄,解析的這個(gè)階段其實(shí)就是將符號(hào)引用轉(zhuǎn)換為可以直接定位對(duì)象等在內(nèi)存中的位置的直接引用。

    運(yùn)行時(shí)常量池位于JVM規(guī)范的方法區(qū)中,在Java8以前,位于永生代;Java8之后位于元空間。

  • 全局字符串常量池(string pool / string literal pool)

    全局字符串池里的內(nèi)容是在類加載完成,經(jīng)過(guò)驗(yàn)證,準(zhǔn)備階段之后在堆中生成字符串對(duì)象實(shí)例,然后將該字符串對(duì)象實(shí)例的引用值存到string pool中。在HotSpot中具體實(shí)現(xiàn)string pool這一功能的是StringTable類,它是一個(gè)哈希表,里面存的是key(字面量“abc”, 即駐留字符串)-value(字符串"abc"實(shí)例對(duì)象在堆中的引用)鍵值對(duì),StringTable本身存在本地內(nèi)存(native memory)中。

    StringTable在每個(gè)HotSpot VM的實(shí)例只有一份,被所有的類共享(享元模式)。在Java7的時(shí)候?qū)⒆址A砍匾频搅硕牙铮瑫r(shí)里面也不在存放對(duì)象(Java7以前被intern的String對(duì)象存放于永生代,所以很容易造成OOM),而是存放堆上String實(shí)例對(duì)象的引用。

那么字符串常量池中引用的String對(duì)象是在什么時(shí)候創(chuàng)建的呢?在JVM規(guī)范里明確指定resolve階段可以是lazy的,即在需要進(jìn)行該符號(hào)引用的解析時(shí)才去解析它,這樣的話,可能該類都已經(jīng)初始化完成了,如果其他的類鏈接到該類中的符號(hào)引用,需要進(jìn)行解析,這個(gè)時(shí)候才會(huì)去解析。

這時(shí)候就需要ldc這個(gè)字節(jié)碼指令,其作用是將int、float或String型常量值從常量池中推送至棧頂,如下面這個(gè)例子。

public class Main {
    public static void main(String[] args) {
      String a="B";
    }
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String B
         2: astore_1
         3: return
      LineNumberTable:
        line 14: 0
        line 15: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
            3       1     1     a   Ljava/lang/String;
}

在main方法的字節(jié)碼中使用ldc將字符串“B”推到棧頂,然后賦值給局部變量a,最后退出。

根據(jù)上面說(shuō)的,在類加載階段,這個(gè) resolve 階段( constant pool resolution )是lazy的。換句話說(shuō)并沒(méi)有真正的對(duì)象,字符串常量池里自然也沒(méi)有,那么ldc指令還怎么把人推送至棧頂?或者換一個(gè)角度想,既然resolve 階段是lazy的,那總有一個(gè)時(shí)候它要真正的執(zhí)行吧,是什么時(shí)候?執(zhí)行l(wèi)dc指令就是觸發(fā)這個(gè)lazy resolution動(dòng)作的條件。

ldc字節(jié)碼在這里的執(zhí)行語(yǔ)義是:到當(dāng)前類的運(yùn)行時(shí)常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找該index對(duì)應(yīng)的項(xiàng),如果該項(xiàng)尚未resolve則resolve之,并返回resolve后的內(nèi)容。

在遇到String類型常量時(shí),resolve的過(guò)程如果發(fā)現(xiàn)StringTable已經(jīng)有了內(nèi)容匹配的java.lang.String的引用,則直接返回這個(gè)引用,反之,如果StringTable里尚未有內(nèi)容匹配的String實(shí)例的引用,則會(huì)在Java堆里創(chuàng)建一個(gè)對(duì)應(yīng)內(nèi)容的String對(duì)象,然后在StringTable記錄下這個(gè)引用,并返回這個(gè)引用出去??梢?jiàn),ldc指令是否需要?jiǎng)?chuàng)建新的String實(shí)例,全看在第一次執(zhí)行這一條ldc指令時(shí),StringTable是否已經(jīng)記錄了一個(gè)對(duì)應(yīng)內(nèi)容的String的引用。

public class Main {
    String a="b";
    public static void main(String[] args) {
    }
}

public com.verzqli.snake.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String b
         7: putfield      #3                  // Field a:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/verzqli/snake/Main;

上面例子執(zhí)行完main方法后,“b”就不會(huì)進(jìn)入字符串常量池。因?yàn)镾tring a = "b"是Main類的成員變量,成員變量只有在執(zhí)行到構(gòu)造方法的時(shí)候才會(huì)初始化。

往細(xì)講,只有執(zhí)行了ldc指令的字符串才會(huì)進(jìn)入字符串常量池

至于ldc指令的工作原理可以看這篇文章。

String.intern()

當(dāng)一個(gè)字符串對(duì)象調(diào)用這個(gè)intern方法時(shí),如果該字符串常量池中不包含該對(duì)象引用,也即StringTable不包含該對(duì)象字面量和引用時(shí),將該字符串對(duì)象引用存入字符串常量中 ,同時(shí)返回該地址。這樣做的目的是為了提升性能,降低開(kāi)銷,后續(xù)如果定義相同字面量的字符串即可返回該引用(內(nèi)存地址),不必再在堆上創(chuàng)建字符串實(shí)例。

  • 實(shí)例(以下實(shí)例環(huán)境為JDK7以后)

          String a="c";
          String b = new String("c");
          System.out.println("a==b.intern()="+(a==b.intern()));
          System.out.println("b==b.intern()="+(b==b.intern()));
          
          結(jié)果:
          a==b.intern()=true
          b==b.intern()=false
    

    類加載階段,什么都沒(méi)干。

    然后運(yùn)行main方法,創(chuàng)建“c”對(duì)象 ,假設(shè)其地址為0xeee,將其加入字符串常量池。隨后在堆上創(chuàng)建了String對(duì)象b,假設(shè)其地址為0xfff

    這里b.intern()檢測(cè)到了字符串常量池中包含“c”這個(gè)字符串引用,所以其返回的是0xeee,而b指向的依舊是0xfff,所以第一個(gè)為true,第二個(gè)為false。

         String a = new String("hellow") + new String("orld");
         String b = new String("hello") + new String("world");
         System.out.println("a==a.intern()="+(a==a.intern()));
         System.out.println("a==b.intern()="+(a==b.intern()));
         System.out.println("b==b.intern()="+(b==b.intern()));
    
       結(jié)果:
      a==b.intern()=true
      a==b.intern()=true
      b==b.intern()=false
    

    類加載階段,什么都沒(méi)干。

    然后運(yùn)行main方法,創(chuàng)建“hellow”,"orld"對(duì)象,并放入字符串常量池。然后會(huì)創(chuàng)建一個(gè)"helloworld"對(duì)象,沒(méi)有放入字符串常量池,a指向這個(gè)"helloworld"對(duì)象(0xeee)。

    接著創(chuàng)建“hello”,"world"對(duì)象,同樣也創(chuàng)建一個(gè)"helloworld"對(duì)象,也沒(méi)有放入字符串常量池,b指向這個(gè)"helloworld"對(duì)象地址(0xfff)。

    這時(shí)候第一個(gè)判斷,字符串常量池沒(méi)有“helloworld”這個(gè)字符串對(duì)象引用,所以將a的引用(0xeee)放入字符串常量池,也就是說(shuō)池子中的引用和a的引用(0xeee)是一樣的,所以a==a.intern()。

    b.intern()時(shí)因?yàn)樯弦徊孔址A砍刂幸呀?jīng)有了這個(gè)“helloworld”的引用,所以他返回回去的引用(0xeee)就是a的引用,所以a==b.intern()。

    從上面可以清楚的知道b.intern()返回的是0xfff,而b引用地址為0xfff,所以b!=b.intern()。

        //        String a1="helloworld";
        String a = new String("hello")+new String("world");
        System.out.println("a==a=" + (a == a.intern()));
    

    這里的結(jié)果如果a1沒(méi)有被注釋則為false,注釋了則為true,原理同上,可以自己腦補(bǔ)一下。

  • JVM對(duì)字符串的優(yōu)化

          String a = "hello";
        String b = a+"world";
        String c = "helloworld";
        String d = "hello"+"world";
        System.out.println(b==c); false
        System.out.println(d==c); true
        System.out.println(b==d); false
        
            Code:
      stack=3, locals=5, args_size=1
         0: ldc           #4                  // String hello //ldc指令創(chuàng)建字符串對(duì)象“hello”
         2: astore_1                          // 將a從放入局部變量表(第一個(gè)局部變量,第0個(gè)是this)
         3: new           #5                  // class java/lang/StringBuilder //創(chuàng)建StringBuilder對(duì)象
         6: dup                               // 復(fù)制棧頂數(shù)據(jù)(創(chuàng)建StringBuilder對(duì)象)壓入棧中
         7: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V 
        10: aload_1                           // 從局部變量中載入a到棧中
        11: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //可以看出字符串相加在字節(jié)碼里就是StringBuilder的append
        14: ldc           #8                  // String world /ldc指令創(chuàng)建字符串對(duì)象“world”
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//繼續(xù)append
        19: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String; //相加完畢,隱形的調(diào)用toString生成String對(duì)象返回
        22: astore_2                          // 將b放入局部變量表(第二個(gè)局部變量)   
        23: ldc           #10                 // String helloworld  //ldc指令創(chuàng)建字符串對(duì)象“helloworld”
        25: astore_3                          // 將c放入局部變量表(第三個(gè)局部變量) 
        26: ldc           #10                 // String helloworld  //這里字符串常量池中已經(jīng)包含了helloworld,就不會(huì)再創(chuàng)建,直接引用,而且這個(gè)helloworld是"hello"+"world"拼接的,這就是JVM對(duì)字符串的優(yōu)化
        28: astore        4                   // 將d放入局部變量表(第四個(gè)局部變量) 
        30: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream; //調(diào)用靜態(tài)方法打印
        33: aload_2                           // 從局部變量表加載b入棧
        34: aload_3                           // 從局部變量表加載c入棧
        35: if_acmpne     42                  // 比較兩個(gè)對(duì)象的引用類型 下面四行就是一個(gè)if else 語(yǔ)句,如果相等就直接doto打印結(jié)果,
        38: iconst_1                          // 獲得兩個(gè)引用是否相等的結(jié)果(true為1,false為0),將1入棧
        39: goto          43                  // 跳轉(zhuǎn)到43行 直接打印出結(jié)果
        42: iconst_0                          // 兩引用不相等,將0入棧 
        43: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
        46: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        后續(xù)都是相同的意思,這里就不注釋了。
        49: aload         4
        51: aload_3
        52: if_acmpne     59
        55: iconst_1
        56: goto          60
        59: iconst_0
        60: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
        63: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        66: aload_2
        67: aload         4
        69: if_acmpne     76
        72: iconst_1
        73: goto          77
        76: iconst_0
        77: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
        80: return
    

    從上面的字節(jié)碼
    可以看出字符串的相加其實(shí)是new了一個(gè)StringBuilder來(lái)進(jìn)行append,a和b不相等就是因?yàn)檫@已經(jīng)是兩個(gè)不同的對(duì)象了,引用也不相等。后續(xù)c和d相等是因?yàn)镴VM對(duì)純字符串想加做了調(diào)優(yōu),會(huì)在字節(jié)碼中把他們直接相加后的值賦給局部變量,所以c和d指向的是同一個(gè)字符串。

    
        String a= "a";
        for (int i = 0; i < 3; i++) {
            a+="b";
        }
        
        Code:
      stack=2, locals=3, args_size=1
         0: ldc           #4                  // String a
         2: astore_1
         3: iconst_0
         4: istore_2
         5: iload_2
         6: iconst_3
         7: if_icmpge     36
        10: new           #5                  // class java/lang/StringBuilder
        13: dup
        14: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        17: aload_1
        18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: ldc           #2                  // String b
        23: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        26: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: astore_1
        30: iinc          2, 1
        33: goto          5
        36: return
    

    對(duì)于for循環(huán)中的字符串相加(3到33行就是for循環(huán)的內(nèi)容),JVM就沒(méi)有優(yōu)化了,每次相加都是重新創(chuàng)建了StringBuilder,開(kāi)銷就是一個(gè)StringBuilder的幾何倍數(shù)那么大,因而在循環(huán)中使用StringBuilder的append來(lái)替代直接相加。

  • 總結(jié)

    除了日常的如果覺(jué)得文章有錯(cuò)誤,歡迎指出并交流。這里問(wèn)一個(gè)問(wèn)題,后續(xù)如果知道了再刪除:字符串常量池和StringTable是一個(gè)東西嗎,兩者都是存的字符串引用,但是R大說(shuō)過(guò)StringTable是存于本地內(nèi)存(native memory),但是看過(guò)的文章都說(shuō)的是字符串常量池位于java堆中,希望有知道的大佬可以告知一下。

  • 引用:

    徹底搞懂string常量池和intern

    JVM 常量池中存儲(chǔ)的是對(duì)象還是引用呢?

    Java String實(shí)例的創(chuàng)建和常量池的關(guān)系及intern方法

    Java 中new String("字面量") 中 "字面量" 是何時(shí)進(jìn)入字符串常量池的?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ??需要說(shuō)明的一點(diǎn)是,這篇文章是以《深入理解Java虛擬機(jī)》第二版這本書(shū)為基礎(chǔ)的,這里假設(shè)大家已經(jīng)了解了JVM的運(yùn)...
    Geeks_Liu閱讀 14,277評(píng)論 5 44
  • 談起String,大家肯定一定都不陌生,肯定也都使用過(guò),出去面試的時(shí)候也有碰到過(guò)問(wèn)相關(guān)原理的。今天就結(jié)合Strin...
    miaoLoveCode閱讀 2,102評(píng)論 10 28
  • JVM中的字符串常量池是個(gè)有些玄幻的玩意兒,關(guān)于它的細(xì)節(jié),各類書(shū)籍和網(wǎng)站上眾說(shuō)紛紜。本文試圖參考盡量權(quán)威的資料,找...
    LittleMagic閱讀 3,206評(píng)論 8 20
  • 這篇文章解釋了Java 虛擬機(jī)(JVM)的內(nèi)部架構(gòu)。下圖顯示了遵守Java SE 7 規(guī)范的典型的 JVM 核心內(nèi)...
    飲墨饗書(shū)閱讀 1,148評(píng)論 0 1
  • 突然發(fā)現(xiàn),一天可以生幾下氣,也可以不說(shuō)一句話。 現(xiàn)在幾乎沒(méi)有吸引我的地方了,我通通都放棄。 不想再去...
    山坑游子閱讀 209評(píng)論 0 0

友情鏈接更多精彩內(nèi)容