JVM筆記:Java虛擬機的常量池

常量池

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

    常量池能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其餘項目關聯最多的數據類型,包含了類也是佔用Class文件中第一個出現的表類型數據項目。java

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

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

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

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

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

    後兩個是字節碼指令,不懂得能夠查閱下相關資料(TODO) + 能夠經過查看字節碼的形式來查看Class的常量池的內容,由於是在編譯時產生的,也能夠稱爲靜態常量池優化

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) {

   }
}
字節碼:
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
複製代碼
  • 運行時常量池

    當java文件被編譯成class文件以後,就會生成上面的常量池,在Class文件中描述的各類信息,最終都須要加載到虛擬機中以後才能運行和使用。 類從被加載到虛擬機內存中開始,到卸載出內存位置,他的生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initalization)、使用(Using)和卸載(Unloading),其中驗證、準備、解析三個部分統稱Wie鏈接(Linking)。ui

    而當類加載到內存中後,JVM就會將Class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每一個類都有一個。在解析過程當中須要將常量池中全部的符號引用(classes、interfaces、fields、methods referenced in the constant pool)轉爲直接引用(獲得類或者字段、方法在內存中的指針或者偏移量,以便直接調用該方法)。直接引用能夠是內存中,直接指向目標的指、相對偏移量,或是一個能間接定位到目標的句柄,解析的這個階段其實就是將符號引用轉換爲能夠直接定位對象等在內存中的位置的直接引用。this

    運行時常量池位於JVM規範的方法區中,在Java8之前,位於永生代;Java8以後位於元空間。spa

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

    全局字符串池裏的內容是在類加載完成,通過驗證,準備階段以後在堆中生成字符串對象實例,而後將該字符串對象實例的引用值存到string pool中。在HotSpot中具體實現string pool這一功能的是StringTable類,它是一個哈希表,裏面存的是key(字面量「abc」, 即駐留字符串)-value(字符串"abc"實例對象在堆中的引用)鍵值對,StringTable自己存在本地內存(native memory)中。.net

    StringTable在每一個HotSpot VM的實例只有一份,被全部的類共享(享元模式)。在Java7的時候將字符串常量池移到了堆裏,同時裏面也不在存放對象(Java7之前被intern的String對象存放於永生代,因此很容易形成OOM),而是存放堆上String實例對象的引用。

    那麼字符串常量池中引用的String對象是在何時建立的呢?在JVM規範裏明確指定resolve階段能夠是lazy的,即在須要進行該符號引用的解析時纔去解析它,這樣的話,可能該類都已經初始化完成了,若是其餘的類連接到該類中的符號引用,須要進行解析,這個時候纔會去解析。

    這時候就須要ldc這個字節碼指令,其做用是將int、float或String型常量值從常量池中推送至棧頂,以下面這個例子。

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方法的字節碼中使用ldc將字符串「B」推到棧頂,而後賦值給局部變量a,最後退出。

根據上面說的,在類加載階段,這個 resolve 階段( constant pool resolution )是lazy的。換句話說並無真正的對象,字符串常量池裏天然也沒有,那麼ldc指令還怎麼把人推送至棧頂?或者換一個角度想,既然resolve 階段是lazy的,那總有一個時候它要真正的執行吧,是何時?執行ldc指令就是觸發這個lazy resolution動做的條件

ldc字節碼在這裏的執行語義是:到當前類的運行時常量池(runtime constant pool,HotSpot VM裏是ConstantPool + ConstantPoolCache)去查找該index對應的項,若是該項還沒有resolve則resolve之,並返回resolve後的內容。

在遇到String類型常量時,resolve的過程若是發現StringTable已經有了內容匹配的java.lang.String的引用,則直接返回這個引用,反之,若是StringTable裏還沒有有內容匹配的String實例的引用,則會在Java堆裏建立一個對應內容的String對象,而後在StringTable記錄下這個引用,並返回這個引用出去。可見,ldc指令是否須要建立新的String實例,全看在第一次執行這一條ldc指令時,StringTable是否已經記錄了一個對應內容的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;
複製代碼

上面例子執行完main方法後,「b」就不會進入字符串常量池。由於String a = "b"是Main類的成員變量,成員變量只有在執行到構造方法的時候纔會初始化。

往細講,只有執行了ldc指令的字符串纔會進入字符串常量池

至於ldc指令的工做原理能夠看這篇文章

String.intern()

當一個字符串對象調用這個intern方法時,若是該字符串常量池中不包含該對象引用,也即StringTable不包含該對象字面量和引用時,將該字符串對象引用存入字符串常量中 ,同時返回該地址。這樣作的目的是爲了提高性能,下降開銷,後續若是定義相同字面量的字符串便可返回該引用(內存地址),沒必要再在堆上建立字符串實例。

  • 實例(如下實例環境爲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()));
          
          結果:
          a==b.intern()=true
          b==b.intern()=false
    複製代碼

    類加載階段,什麼都沒幹。

    而後運行main方法,建立「c」對象 ,假設其地址爲0xeee,將其加入字符串常量池。隨後在堆上建立了String對象b,假設其地址爲0xfff

    這裏b.intern()檢測到了字符串常量池中包含「c」這個字符串引用,因此其返回的是0xeee,而b指向的依舊是0xfff,因此第一個爲true,第二個爲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()));
    
       結果:
      a==b.intern()=true
      a==b.intern()=true
      b==b.intern()=false
    複製代碼

    類加載階段,什麼都沒幹。

    而後運行main方法,建立「hellow」,"orld"對象,並放入字符串常量池。而後會建立一個"helloworld"對象,沒有放入字符串常量池,a指向這個"helloworld"對象(0xeee)。

    接着建立「hello」,"world"對象,一樣也建立一個"helloworld"對象,也沒有放入字符串常量池,b指向這個"helloworld"對象地址(0xfff)。

    這時候第一個判斷,字符串常量池沒有「helloworld」這個字符串對象引用,因此將a的引用(0xeee)放入字符串常量池,也就是說池子中的引用和a的引用(0xeee)是同樣的,因此a==a.intern()

    b.intern()時由於上一部字符串常量池中已經有了這個「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()));
    複製代碼

    這裏的結果若是a1沒有被註釋則爲false,註釋了則爲true,原理同上,能夠本身腦補一下。

  • JVM對字符串的優化

    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指令建立字符串對象「hello」
         2: astore_1                          // 將a從放入局部變量表(第一個局部變量,第0個是this)
         3: new           #5 // class java/lang/StringBuilder //建立StringBuilder對象
         6: dup                               // 複製棧頂數據(建立StringBuilder對象)壓入棧中
         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; //能夠看出字符串相加在字節碼裏就是StringBuilder的append
        14: ldc           #8 // String world /ldc指令建立字符串對象「world」
        16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//繼續append
        19: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; //相加完畢,隱形的調用toString生成String對象返回
        22: astore_2                          // 將b放入局部變量表(第二個局部變量)   
        23: ldc           #10 // String helloworld //ldc指令建立字符串對象「helloworld」
        25: astore_3                          // 將c放入局部變量表(第三個局部變量) 
        26: ldc           #10 // String helloworld //這裏字符串常量池中已經包含了helloworld,就不會再建立,直接引用,並且這個helloworld是"hello"+"world"拼接的,這就是JVM對字符串的優化
        28: astore        4                   // 將d放入局部變量表(第四個局部變量) 
        30: getstatic     #11 // Field java/lang/System.out:Ljava/io/PrintStream; //調用靜態方法打印
        33: aload_2                           // 從局部變量表加載b入棧
        34: aload_3                           // 從局部變量表加載c入棧
        35: if_acmpne     42                  // 比較兩個對象的引用類型 下面四行就是一個if else 語句,若是相等就直接doto打印結果,
        38: iconst_1                          // 得到兩個引用是否相等的結果(true爲1,false爲0),將1入棧
        39: goto          43                  // 跳轉到43行 直接打印出結果
        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;
        後續都是相同的意思,這裏就不註釋了。
        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
    複製代碼

    從上面的字節碼能夠看出字符串的相加實際上是new了一個StringBuilder來進行append,a和b不相等就是由於這已是兩個不一樣的對象了,引用也不相等。後續c和d相等是由於JVM對純字符串想加作了調優,會在字節碼中把他們直接相加後的值賦給局部變量,因此c和d指向的是同一個字符串。

    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
    複製代碼

    對於for循環中的字符串相加(3到33行就是for循環的內容),JVM就沒有優化了,每次相加都是從新建立了StringBuilder,開銷就是一個StringBuilder的幾何倍數那麼大,於是在循環中使用StringBuilder的append來替代直接相加。

  • 總結

    除了平常的若是以爲文章有錯誤,歡迎指出並交流。這裏問一個問題,後續若是知道了再刪除:字符串常量池和StringTable是一個東西嗎,二者都是存的字符串引用,可是R大說過StringTable是存於本地內存(native memory),可是看過的文章都說的是字符串常量池位於java堆中,但願有知道的大佬能夠告知一下。

  • 引用:

    完全搞懂string常量池和intern

    JVM 常量池中存儲的是對象仍是引用呢?

    Java String實例的建立和常量池的關係及intern方法

    Java 中new String("字面量") 中 "字面量" 是什麼時候進入字符串常量池的?

相關文章
相關標籤/搜索