java 爲何須要常量池

java中講的常量池,一般指的是運行時常量池,它是方法區的一部分,一個jvm實例只有一個運行常量池,各線程間共享該運行常量池。java

java內存模型中將內存分爲堆和棧,其中堆爲線程間共享的內存數據區域,棧爲線程間私有的內存區域。堆又包括方法區以及非方法區部分,棧包括本地方法棧、虛擬機棧等,以下圖所示:c++

圖片描述

爲何須要常量池

jvm 在棧幀(frame) 中進行操做數和方法的動態連接(link),爲了便於連接,jvm 使用常量池來保存跟蹤當前類中引用的其餘類及其成員變量和成員方法。app

每一個棧幀(frame)都包含一個運行常量池的引用,這個引用指向當前棧幀須要執行的方法,jvm使用這個引用來進行動態連接。jvm

在 c/c++ 中,編譯器將多個編譯期編譯的文件連接成一個可執行文件或者dll文件,在連接階段,符號引用被解析爲實際地址。java 中這種連接是在程序運行時動態進行的。ui

圖片描述

常量池探祕

每一個 java 文件編譯爲 class 文件後,都將產生當前類獨有的常量池,咱們稱之爲靜態常量池。class 文件中的常量池包含兩部分:字面值(literal)和符號引用(Symbolic Reference)。其中字面值能夠理解爲 java 中定義的字符串常量、final 常量等;符號引用指的是一些字符串,這些字符串表示當前類引用的外部類、方法、變量等的引用地址的抽象表示形式,在類被jvm裝載並第一次使用這些符號引用時,這些符號引用將會解析爲直接引用。符號常量包含:編碼

  • 類和接口的全限定名spa

  • 字段的名稱和描述符線程

  • 方法的名稱和描述符code

jvm在進行類裝載時,將class文件中常量池部分的常量加載到方法區中,此時方法區中的保存常量的邏輯區域稱之爲運行時常量區。對象

使用javap -verbose 命令能夠查看class字節碼的詳細信息,其中包含了編譯期肯定的靜態常量池。

public class StringTest {
    
    public static void main(String[] args){
        String s = new String("abc");
        String s2 = s.intern();
        System.out.println(s2 == s);

        String s3 = (s + s2);

        System.out.println(s3 == s3.intern());

    }
}

上述代碼javap -verbose後獲得(只拿出常量池部分):

major version: 52
Constant pool:
   #1 = Methodref          #13.#26        // java/lang/Object."<init>":()V
   #2 = Class              #27            // java/lang/String
   #3 = String             #28            // abc
   #4 = Methodref          #2.#29         // java/lang/String."<init>":(Ljava/lang/String;)V
   #5 = Methodref          #2.#30         // java/lang/String.intern:()Ljava/lang/String;
   #6 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #33.#34        // java/io/PrintStream.println:(Z)V
   #8 = Class              #35            // java/lang/StringBuilder
   #9 = Methodref          #8.#26         // java/lang/StringBuilder."<init>":()V
  #10 = Methodref          #8.#36         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #8.#37         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #12 = Class              #38            // StringTest
  #13 = Class              #39            // java/lang/Object
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               StackMapTable
  #21 = Class              #40            // "[Ljava/lang/String;"
  #22 = Class              #27            // java/lang/String
  #23 = Class              #41            // java/io/PrintStream
  #24 = Utf8               SourceFile
  #25 = Utf8               StringTest.java
  #26 = NameAndType        #14:#15        // "<init>":()V
  #27 = Utf8               java/lang/String
  #28 = Utf8               abc
  #29 = NameAndType        #14:#42        // "<init>":(Ljava/lang/String;)V
  #30 = NameAndType        #43:#44        // intern:()Ljava/lang/String;
  #31 = Class              #45            // java/lang/System
  #32 = NameAndType        #46:#47        // out:Ljava/io/PrintStream;
  #33 = Class              #41            // java/io/PrintStream
  #34 = NameAndType        #48:#49        // println:(Z)V
  #35 = Utf8               java/lang/StringBuilder
  #36 = NameAndType        #50:#51        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #37 = NameAndType        #52:#44        // toString:()Ljava/lang/String;
  #38 = Utf8               StringTest
  #39 = Utf8               java/lang/Object
  #40 = Utf8               [Ljava/lang/String;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               (Ljava/lang/String;)V
  #43 = Utf8               intern
  #44 = Utf8               ()Ljava/lang/String;
  #45 = Utf8               java/lang/System
  #46 = Utf8               out
  #47 = Utf8               Ljava/io/PrintStream;
  #48 = Utf8               println
  #49 = Utf8               (Z)V
  #50 = Utf8               append
  #51 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Utf8               toString

咱們能夠看到,常量池共包含52個常量。#1 是一個類中方法的符號引用,它由 #13#26 兩個utf8編碼的字符串構成;#3 是程序中定義的 String 類型的字面值 "abc",它包含指向一個utf8編碼字符串 "abc" 的索引 #28

方法的調用、成員變量的訪問最終都是經過運行時常量池來查找具體地址的。

String 常量池

運行時常量池有一種 String 類型的常量,即一般咱們所說的字符串字面值,全部的字符串字面值組成一個 String 常量表。String常量表並非一成不變的,程序運行時能夠動態添加字符串常量,使用String的intern()能夠動態的添加String常量。但

jvm 確保兩個在值上徹底相等的字符串字面值(即其中包含的字符序列是相同的,使用equals()來判斷)指向同一個 String 實例。

如:

String s1 = "abc";

String s2 = "abc";

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

上述代碼中的字符串 s1 和 s2 將指向同一個 String 實例。實際上經過查看class文件,咱們能夠看到,在編譯後,靜態常量池中已經包含了一個 String 類型的字面值 "abc",程序運行時只是從常量池中獲取這個String字面值的引用地址,並賦值給變量 s1 和變量 s2。

Constant pool:
   #1 = Methodref          #6.#19         // java/lang/Object."<init>":()V
   #2 = String             #20            // abc
   ······
   #20 = Utf8               abc

public static void main(java.lang.String[]);
    ······
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: ldc           #2                  // String abc

其中,ldc 表示將一個常量加載到操做數棧。

String 的 intern() 是一個native方法,返回的是一個String對象的標準表示。當調用該方法時,若是運行時常量池中已經存在與之相等(equal())的字符串,則直接返回常量池中的字符串引用,不然將此字符串添加到池中,並返回。

String s1 = "abc";

String s2 = new String("abc");

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

System.out.println(s1.equals(s2));      //返回 true

System.out.println(s1 == s2.intern());  //返回 true

上述代碼中,雖然 s1 和 s2 中的值是相同的,可是他們指向的並非同一個對象,但 s2 的標準化表示和s1是同一個 String 對象,都是編譯期肯定的常量池中的 "abc"。

相關文章
相關標籤/搜索