天天看點

【JVM】探究Java常量本質及三種常量池

可以從他人的博文,還有一些書籍中了解到

常量是放在常量池

中,細節的内容無從得知,相信每個人都會覺得面前的東西是一個幾乎完全的黑盒,總是覺得不舒服,翻閱《深入了解Java虛拟機》,會發現這本書中對常量的介紹更多地偏重于位元組碼檔案的結構,還有在自動記憶體管理機制中也介紹了運作時常量池。下面換種思路來看一下

Java中的常量池分為三種形态:靜态常量池,字元串常量池以及運作時常量池。

? 靜态常量池

所謂

靜态常量池

,即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字元串(數字)字面量,還包含類、方法的資訊,占用class檔案絕大部分空間。

這種常量池主要用于存放兩大類常量:字面量(Literal)和符号引用量(Symbolic References),字面量相當于Java語言層面常量的概念,如文本字元串,聲明為final的常量值等,符号引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:

  • 類和接口的全限定名
  • 字段名稱和描述符
  • 方法名稱和描述符

而運作時常量池,則是jvm虛拟機在完成類裝載操作後,将class檔案中的常量池載入到記憶體中,并儲存在方法區中,我們常說的常量池,就是指方法區中的運作時常量池。

運作時常量池相對于Class檔案常量池的另外一個重要特征是具備動态性,Java語言并不要求常量一定隻有編譯期才能産生,也就是并非預置入class檔案中常量池的内容才能進入方法區運作時常量池,運作期間也可能将新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。

String的intern()方法會查找在常量池中是否存在一份equal相等的字元串,如果有則傳回該字元串的引用,如果沒有則添加自己的字元串進入常量池。

那這樣來看,通過靜态常量池,即*.class檔案中的常量池 更能夠探究常量的含義了

下面看一段代碼

public class Main {

    public static void main(String[] args) {
        System.out.println(Father.str);
    }
}

class Father{
    public static String str = "Hello,world";
    static {
        System.out.println("Father static block");
    }
}           

輸出結果為

【JVM】探究Java常量本質及三種常量池

再看另一個:

package com.company;

public class Main {

    public static void main(String[] args) {
        System.out.println(Father.str);
    }
}

class Father{
    public static final String str = "Hello,world";
    static {
        System.out.println("Father static block");
    }
}           

結果:

隻有一個

【JVM】探究Java常量本質及三種常量池

是不是發現很吃驚啊

我們對第二個示範的代碼塊進行反編譯一下

D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
  public com.company.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Hello,world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}           

這裡有一個Main()是構造方法 下面的是main方法

0: getstatic # 2 對應的是System.out

3: ldc #4 對應的值 直接是 Hello,world 了 确定的值 沒有從Father類中取出

ldc表示将int,float或是String類型的常量值從常量池中推送至棧頂

竟然沒有!!! 即使删除Father.class檔案 這段代碼照樣可以運作 它和Father類 沒有半毛錢的關系了

實際上,在編譯階段 常量就會被存入到調用這個常量的方法所在的類的常量池當中

從這個例子中 可以看出 這裡的str 是一個常量 調用這個常量的方法是main方法 main方法所在的類是Main ,也就是說編譯之後str被放在了該類的常量池中

本質上,調用類并沒有直接引用到定義常量的類,是以并不會觸發定義常量的類的初始化

類的初始化 涉及到類的加載機制 這裡暫時寫不說 這個留到之後必須要好好說說

? 字元串常量池(string pool也有叫做string literal pool)

全局字元串池裡的内容是在類加載完成,經過驗證,準備階段之後在堆中生成字元串對象執行個體,然後将該字元串對象執行個體的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的執行個體對象,具體的執行個體對象是在堆中開辟的一塊空間存放的。)。

字元串常量池的位置的說法不太準确

在JDK6.0及之前版本,字元串常量池是放在Perm Gen區(也就是方法區)中;

在JDK7.0版本,字元串常量池被移到了堆中了。

在HotSpot VM裡實作的string pool功能的是一個StringTable類,它是一個哈希表,裡面存的是駐留字元串(也就是我們常說的用雙引号括起來的)的引用(而不是駐留字元串執行個體本身),也就是說在堆中的某些字元串執行個體被這個StringTable引用之後就等同被賦予了”駐留字元串”的身份。這個StringTable在每個HotSpot VM的執行個體隻有一份,被所有的類共享。

? 回到運作常量池(runtime constant pool)

jvm在執行某個類的時候,必須經過加載、連接配接、初始化,而連接配接又包括驗證、準備、解析三個階段。

而當類加載到記憶體中後,jvm就會将靜态常量池中的内容存放到運作時常量池中,由此可知,運作時常量池也是每個類都有一個。

靜态常量池中存的是字面量和符号引用,也就是說它們存的并不是對象的執行個體,而是對象的符号引用值。而經過解析(resolve)之後,也就是把符号引用替換為直接引用,解析的過程會去查詢字元串常量池,也就是我們上面所說的StringTable,以保證運作時常量池所引用的字元串與字元串常量池中所引用的是一緻的。

我們看一個例子

import java.util.UUID;

public class Test {
    public static void main(String[] args) {
        System.out.println(TestValue.str);
    }
}

class TestValue{
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("TestValue static code");
    }
}
           
【JVM】探究Java常量本質及三種常量池

從聲明本身str都是常量,關鍵的是這個常量的值能否在編譯時期确定下來,顯然這裡的例子在編譯期的時候顯然是确定不下來的。需要在運作期才能能夠确定下來,這要求目标類要進行初始化

當常量的值并非編譯期間可以确定的,那麼其值不會被放到調用類的常量池中

這時在程式運作時,會導緻主動使用這個常量所在的類,顯然會導緻這個類被初始化。

(這個涉及到類的加載機制,後面會寫這裡做個标記)

反編譯探究一下:

Compiled from "Test.java"
class com.leetcodePractise.tstudy.TestValue {
  public static final java.lang.String str;

  com.leetcodePractise.tstudy.TestValue();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: invokestatic  #2                  // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
       3: invokevirtual #3                  // Method java/util/UUID.toString:()Ljava/lang/String;
       6: putstatic     #4                  // Field str:Ljava/lang/String;
       9: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #6                  // String TestValue static code
      14: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: return
}           

很明顯TestValue類會初始化出來

常量介紹完之後 這裡記錄一下反編譯及助記符的筆記

package com.company;

public class Main {

    public static void main(String[] args) {
        System.out.println(Father.str);
        System.out.println(Father.s);
    }
}

class Father{
    public static final String str = "Hello,world";
    public static final short s = 6;
    static {
        System.out.println("Father static block");
    }
}           
【JVM】探究Java常量本質及三種常量池
public class com.company.Main {
  public com.company.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Hello,world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: bipush        6
      13: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      16: return
}           
bipush 表示将單位元組(-128-127)的常量值推送至棧頂

再加入

package com.company;

public class Main {

    public static void main(String[] args) {
        System.out.println(Father.str);
        System.out.println(Father.s);
        System.out.println(Father.t);
    }
}

class Father{
    public static final String str = "Hello,world";
    public static final short s = 6;
    public static final int t = 128;
    static {
        System.out.println("Father static block");
    }
}           

進行反編譯

public class com.company.Main {
  public com.company.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Hello,world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: bipush        6
      13: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: sipush        128
      22: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      25: return
}           
sipush表示将一個短整型常量值(-32768~32767)推送至棧頂

再進行更改

package com.company;

public class Main {

    public static void main(String[] args) {
        System.out.println(Father.str);

        System.out.println(Father.t);
    }
}

class Father{
    public static final String str = "Hello,world";

    public static final int t = 1;
    static {
        System.out.println("Father static block");
    }
}           
public class com.company.Main {
  public com.company.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Hello,world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: bipush        6
      13: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: sipush        128
      22: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      25: return
}

D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
  public com.company.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String Hello,world
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: iconst_1
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      15: return
}           

這裡變成了 iconst_1

iconst 1表示将int類型1推送至棧頂(iconst_m1-iconst_5)

當大于5的時候 就變為了bipush

m1對應的是-1