JVM常量池淺析

提綱.png

??需要說(shuō)明的一點(diǎn)是,這篇文章是以《深入理解Java虛擬機(jī)》第二版這本書(shū)為基礎(chǔ)的,這里假設(shè)大家已經(jīng)了解了JVM的運(yùn)行時(shí)區(qū)域,以及class文件結(jié)構(gòu),類(lèi)加載流程等基礎(chǔ)內(nèi)容。當(dāng)然,文中我們也會(huì)提一提相關(guān)的內(nèi)容作為復(fù)習(xí)總結(jié)

一.JVM有幾種常量池

??主要分為:Class文件常量池、運(yùn)行時(shí)常量池,當(dāng)然還有全局字符串常量池,以及基本類(lèi)型包裝類(lèi)對(duì)象常量池

1.Class文件常量池

??閱讀過(guò)《深入理解Java虛擬機(jī)》這本書(shū)第6章內(nèi)容的小伙伴肯定知道,class文件是一組以8位字節(jié)為單位的二進(jìn)制數(shù)據(jù)流,在java代碼的編譯期間,我們編寫(xiě)的.java文件就被編譯為.class文件格式的二進(jìn)制數(shù)據(jù)存放在磁盤(pán)中,其中就包括class文件常量池
??class 文件中存在常量池(非運(yùn)行時(shí)常量池),其在編譯階段就已經(jīng)確定;JVM 規(guī)范對(duì) class 文件結(jié)構(gòu)有著嚴(yán)格的規(guī)范,必須符合此規(guī)范的 class 文件才會(huì)被 JVM 認(rèn)可和裝載。

為了方便說(shuō)明,我們這里先寫(xiě)一個(gè)很簡(jiǎn)單的類(lèi):

class JavaBean{
    private int value = 1;
    public String s = "abc";
    public final static int f = 0x101;

    public void setValue(int v){
        final int temp = 3;
        this.value = temp + v;
    }

    public int getValue(){
        return value;
    }
}

通過(guò)javah命令編譯之后,用javap -v 命令查看編譯后的文件:

class JavaBasicKnowledge.JavaBean
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#29         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#30         // JavaBasicKnowledge/JavaBean.value:I
   #3 = String             #31            // abc
   #4 = Fieldref           #5.#32         // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String;
   #5 = Class              #33            // JavaBasicKnowledge/JavaBean
   #6 = Class              #34            // java/lang/Object
   #7 = Utf8               value
   #8 = Utf8               I
   #9 = Utf8               s
  #10 = Utf8               Ljava/lang/String;
  #11 = Utf8               f
  #12 = Utf8               ConstantValue
  #13 = Integer            257
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               LJavaBasicKnowledge/JavaBean;
  #21 = Utf8               setValue
  #22 = Utf8               (I)V
  #23 = Utf8               v
  #24 = Utf8               temp
  #25 = Utf8               getValue
  #26 = Utf8               ()I
  #27 = Utf8               SourceFile
  #28 = Utf8               StringConstantPool.java
  #29 = NameAndType        #14:#15        // "<init>":()V
  #30 = NameAndType        #7:#8          // value:I
  #31 = Utf8               abc
  #32 = NameAndType        #9:#10         // s:Ljava/lang/String;
  #33 = Utf8               JavaBasicKnowledge/JavaBean
  #34 = Utf8               java/lang/Object

可以看到這個(gè)命令之后我們得到了該class文件的版本號(hào)、常量池、已經(jīng)編譯后的字節(jié)碼指令(處于篇幅原因這里省略),下面我們會(huì)對(duì)照這個(gè)class文件來(lái)講解:

??這里我們需要說(shuō)明一下,既然是常量池,那么其中個(gè)存放的肯定是“常量”,那么什么是“常量”呢?class文件常量池主要存放兩大常量:字面量符號(hào)引用

1).字面量

字面量接近于java語(yǔ)言層面的常量概念,主要包括:

  • 文本字符串,也就是我們經(jīng)常聲明的:public String s = "abc";中的"abc"
 #9 = Utf8               s
 #3 = String             #31            // abc
 #31 = Utf8              abc
  • 用final修飾的成員變量,包括靜態(tài)變量、實(shí)例變量局部變量
 #11 = Utf8               f
 #12 = Utf8               ConstantValue
 #13 = Integer            257

??這里需要說(shuō)明的一點(diǎn),上面說(shuō)的存在于常量池的字面量,指的是數(shù)據(jù)的,也就是abc0x101(257),通過(guò)上面對(duì)常量池的觀察可知這兩個(gè)字面量是確實(shí)存在于常量池的。
??而對(duì)于基本類(lèi)型數(shù)據(jù)(甚至是方法中的局部變量),也就是上面的private int value = 1;常量池中只保留了他的的字段描述符I字段的名稱(chēng)value,他們的字面量不會(huì)存在于常量池:

2).符號(hào)引用

符號(hào)引用主要設(shè)涉及編譯原理方面的概念,包括下面三類(lèi)常量:

  • 類(lèi)和接口全限定名,也就是Ljava/lang/String;這樣,將類(lèi)名中原來(lái)的"."替換為"/"得到的,主要用于在運(yùn)行時(shí)解析得到類(lèi)的直接引用,像上面:
 #5 = Class              #33            // JavaBasicKnowledge/JavaBean
 #33 = Utf8               JavaBasicKnowledge/JavaBean
  • 字段名稱(chēng)描述符,字段也就是類(lèi)或者接口中聲明的變量,包括類(lèi)級(jí)別變量(static)實(shí)例級(jí)的變量
 #4 = Fieldref           #5.#32         // JavaBasicKnowledge/JavaBean.value:I
 #5 = Class              #33            // JavaBasicKnowledge/JavaBean
 #32 = NameAndType       #7:#8          // value:I

 #7 = Utf8               value
 #8 = Utf8               I

 //這兩個(gè)是局部變量,值保留字段名稱(chēng)
 #23 = Utf8               v
 #24 = Utf8               temp

可以看到,class文件的常量池中也存在方法中的局部變量,但是沒(méi)有;但是常量池外面的字段表中不包括局部變量;

  • 方法名稱(chēng)描述符,方法的描述類(lèi)似于JNI動(dòng)態(tài)注冊(cè)時(shí)的“方法簽名”,也就是參數(shù)類(lèi)型+返回值類(lèi)型:
  #21 = Utf8               setValue
  #22 = Utf8               (I)V

  #25 = Utf8               getValue
  #26 = Utf8               ()I

2.運(yùn)行時(shí)常量池

??運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以也是全局共享的。我們知道,jvm在執(zhí)行某個(gè)類(lèi)的時(shí)候,必須經(jīng)過(guò)加載、連接(驗(yàn)證,準(zhǔn)備,解析)、初始化,在第一步的加載階段,虛擬機(jī)需要完成下面3件事情:

  • 通過(guò)一個(gè)類(lèi)的“全限定名”來(lái)獲取此類(lèi)的二進(jìn)制字節(jié)流
  • 將這個(gè)字節(jié)流所代表的靜態(tài)儲(chǔ)存結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
  • 在內(nèi)存中生成一個(gè)類(lèi)代表這類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)訪(fǎng)問(wèn)的入口

??這里需要說(shuō)明的一點(diǎn)是,類(lèi)對(duì)象和普通的實(shí)例對(duì)象是不同的,類(lèi)對(duì)象是在類(lèi)加載的時(shí)候生成的,普通的實(shí)例對(duì)象一般是在調(diào)用new之后創(chuàng)建。

??上面第二條,將class字節(jié)流代表的靜態(tài)儲(chǔ)存結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),其中就包含了class文件常量池進(jìn)入運(yùn)行時(shí)常量池的過(guò)程。這里需要強(qiáng)調(diào)一下,不同的類(lèi)共用一個(gè)運(yùn)行時(shí)常量池(http://blog.csdn.net/fan2012huan/article/details/52759614),同時(shí)在進(jìn)入運(yùn)行時(shí)常量池的過(guò)程中,多個(gè)class文件中常量池中相同的字符串只會(huì)存在一份在運(yùn)行時(shí)常量池中,這也是一種優(yōu)化。

??運(yùn)行時(shí)常量池的作用是存儲(chǔ) Java class文件常量池中的符號(hào)信息。運(yùn)行時(shí)常量池 中保存著一些 class 文件中描述的符號(hào)引用,同時(shí)在類(lèi)加載的“解析階段”還會(huì)將這些符號(hào)引用所翻譯出來(lái)的直接引用(直接指向?qū)嵗龑?duì)象的指針)存儲(chǔ)在 運(yùn)行時(shí)常量池 中。

??運(yùn)行時(shí)常量池相對(duì)于 class 常量池一大特征就是其具有動(dòng)態(tài)性,Java 規(guī)范并不要求常量只能在運(yùn)行時(shí)才產(chǎn)生,也就是說(shuō)運(yùn)行時(shí)常量池中的內(nèi)容并不全部來(lái)自 class 常量池,class 常量池并非運(yùn)行時(shí)常量池的唯一數(shù)據(jù)輸入口;在運(yùn)行時(shí)可以通過(guò)代碼生成常量并將其放入運(yùn)行時(shí)常量池中,這種特性被用的較多的是String.intern()(這個(gè)方法下面將會(huì)詳細(xì)講)。

二.全局字符串常量池

??字符串常量池單獨(dú)列出來(lái)說(shuō)有兩個(gè)原因:

  • 不同于基本數(shù)據(jù)類(lèi)型,String類(lèi)型是一個(gè)final對(duì)象,他的字面量存在于class文件常量池中,但是運(yùn)行期行為卻與普通常量不同
  • JDK 1.7中,字符串常量池和類(lèi)引用被移動(dòng)到了Java堆中(與運(yùn)行時(shí)常量池分離),因此不同版本的String行為也有所差異

1.Java中創(chuàng)建字符串對(duì)象的兩種方式

??這個(gè)問(wèn)題我想大家一定非常清楚了吧,一般有如下兩種:

  • String s0 =”hellow”;
  • String s1=new String (“hellow”);

??第一種我們之前已經(jīng)見(jiàn)過(guò)了,這種方式聲明的字面量hellow是在編譯期就已經(jīng)確定的,它會(huì)直接進(jìn)入class文件常量池中;當(dāng)運(yùn)行期間在全局字符串常量池中會(huì)保存它的一個(gè)引用,實(shí)際上最終還是要在堆上創(chuàng)建一個(gè)”hellow”對(duì)象,這個(gè)后面會(huì)講。
??第二種方式方式使用了new String(),也就是調(diào)用了String類(lèi)的構(gòu)造函數(shù),我們知道new指令是創(chuàng)建一個(gè)類(lèi)的實(shí)例對(duì)象并完成加載初始化的,因此這個(gè)字符串對(duì)象是在運(yùn)行期才能確定的,創(chuàng)建的字符串對(duì)象是在堆內(nèi)存上
??因此此時(shí)調(diào)用System.out.println(s0 == s1);返回的肯定是flase,因此==符號(hào)比較的是兩邊元素的地址,s1和s0都存在于堆上,但是地址肯定不相同。

下面我們來(lái)看看幾個(gè)非常常見(jiàn)的題目:

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
1) s1 == s2

??這個(gè)對(duì)比第一部分常量池的講解應(yīng)該很好理解,因?yàn)樽置媪?code>"Hello"在運(yùn)行時(shí)會(huì)進(jìn)入運(yùn)行時(shí)常量池(中的字符串常量池,JDK1.7以前),同時(shí)同一份字面量只會(huì)保留一份,所有引用都指向這一份字符串,自然引用的地址也就相同了。

2) s1 == s3

??這個(gè)主要牽扯String"+"號(hào)編譯器優(yōu)化的問(wèn)題,s3雖然是動(dòng)態(tài)拼接出來(lái)的字符串,但是所有參與拼接的部分都是已知的字面量,在編譯期間,這種拼接會(huì)被優(yōu)化,編譯器直接幫你拼好,因此String s3 = "Hel" + "lo";在class文件中被優(yōu)化成String s3 = "Hello";,所以s1 == s3成立。

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String Hello
         2: astore_1
         3: ldc           #2                  // String Hello
         5: astore_2
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: aload_1
        10: aload_2
        11: if_acmpne     18
        14: iconst_1
        15: goto          19
        18: iconst_0
        19: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
        22: return

??通過(guò)查看編譯后的方法代碼,可以看到這里加入操作數(shù)棧的ldc指令有兩次,都是“Hello”,沒(méi)有出現(xiàn)“Hel”或者“l(fā)o”,同時(shí)這兩個(gè)“Hello”指向常量池的通過(guò)一個(gè)地址,都是#2,因此常量池中也只存在一個(gè)“Hello”字面量。

3) s1 != s4

??其實(shí)這個(gè)也不難理解,但是我們還是先來(lái)看看編譯后的字節(jié)碼:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=1
         0: ldc           #2                  // String Hello
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String Hel
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: new           #7                  // class java/lang/String
        18: dup
        19: ldc           #8                  // String lo
        21: invokespecial #9                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        24: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        27: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        30: astore_2
        31: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        34: aload_1
        35: aload_2
        36: if_acmpne     43
        39: iconst_1
        40: goto          44
        43: iconst_0
        44: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
        47: return

??我們就不對(duì)操作符一一解釋了,可以看到這次確實(shí)出現(xiàn)了“String Hel”“String lo”,原因上面我們也說(shuō)過(guò),這是因?yàn)?code>new String("lo")在堆中new了一個(gè)String對(duì)象出來(lái),而“Hel”字面量是通過(guò)另一種操作在堆中創(chuàng)建的對(duì)象,這兩個(gè)在堆中不同地方創(chuàng)建的對(duì)象是通過(guò)StringBuilder.append方法拼接出來(lái)的,并且最終會(huì)調(diào)用StringBuilder.toString方法輸出(最終輸出的也是“Hello”),這些通過(guò)上面字節(jié)碼的分析都可以看得出來(lái),我們來(lái)看看StringBuilder.toString方法:

@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

??可以看到,這個(gè)最終是拼接出來(lái)的一個(gè)String對(duì)象,也就是說(shuō),s4指向的一個(gè)經(jīng)過(guò)StringBuilder拼接之后的String對(duì)象,而s1指向的是另一個(gè)對(duì)象,這兩個(gè)對(duì)象的地址當(dāng)然是不同的了。

4) s1 != s9
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: ldc           #2                  // String Hello
         2: astore_1
         3: ldc           #3                  // String H
         5: astore_2
         6: ldc           #4                  // String ello
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_2
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_3
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore        4
        29: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        32: aload_1
        33: aload         4
        35: if_acmpne     42
        38: iconst_1
        39: goto          43
        42: iconst_0
        43: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        46: return

??從變異后的字節(jié)碼看,這和3)中的情況是相同的,都是通過(guò)StringBuilder.append拼接后toString輸出的全新對(duì)象,至于這個(gè)對(duì)象被分配到哪里去了,我們也不知道。

2.String s1 = "Hello",到底有沒(méi)有在堆中創(chuàng)建對(duì)象?

JVM運(yùn)行時(shí)數(shù)據(jù)區(qū).png

??上面這張圖比是我們通常理解的JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的結(jié)構(gòu),但是還有不完整的地方,為了說(shuō)明全局字符串常量池概念,就必須拿出下面這張圖:

JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)Plus.png

這張圖中,可以看到,方法區(qū)實(shí)際上是在一塊叫“非堆”的區(qū)域包含——可以簡(jiǎn)單粗略的理解為非堆中包含了永生代,而永生代中又包含了方法區(qū)和字符串常量池,我們放大一下,一遍大家看的更清楚些:

非堆.png

??其中的Interned String就是全局共享的“字符串常量池(String Pool)”,和運(yùn)行時(shí)常量池不是一個(gè)概念。但我們?cè)诖a中申明String s1 = "Hello";這句代碼后,在類(lèi)加載的過(guò)程中,類(lèi)的class文件的信息會(huì)被解析到內(nèi)存的方法區(qū)里。
??class文件里常量池里大部分?jǐn)?shù)據(jù)會(huì)被加載到“運(yùn)行時(shí)常量池”,包括String的字面量;但同時(shí)“Hello”字符串的一個(gè)引用會(huì)被存到同樣在“非堆”區(qū)域的“字符串常量池”中,而"Hello"本體還是和所有對(duì)象一樣,創(chuàng)建在Java堆中。

??當(dāng)主線(xiàn)程開(kāi)始創(chuàng)建s1時(shí),虛擬機(jī)會(huì)先去字符串池中找是否有equals(“Hello”)的String,如果相等就把在字符串池中“Hello”的引用復(fù)制給s1;如果找不到相等的字符串,就會(huì)在堆中新建一個(gè)對(duì)象,同時(shí)把引用駐留在字符串池,再把引用賦給str。
??當(dāng)用字面量賦值的方法創(chuàng)建字符串時(shí),無(wú)論創(chuàng)建多少次,只要字符串的值相同,它們所指向的都是堆中的同一個(gè)對(duì)象。

字符串常量池的本質(zhì)

??看到這里,是時(shí)候引出字符串常量池的概念了:字符串常量池是JVM所維護(hù)的一個(gè)字符串實(shí)例的引用表,在HotSpot VM中,它是一個(gè)叫做StringTable的全局表。在字符串常量池中維護(hù)的是字符串實(shí)例的引用,底層C++實(shí)現(xiàn)就是一個(gè)Hashtable。這些被維護(hù)的引用所指的字符串實(shí)例,被稱(chēng)作”被駐留的字符串”或”interned string”或通常所說(shuō)的”進(jìn)入了字符串常量池的字符串”。
??再?gòu)?qiáng)調(diào)一遍:運(yùn)行時(shí)常量池在方法區(qū)(Non-heap),而JDK1.7后,字符串常量池被移到了heap區(qū),因此兩者根本就不是一個(gè)概念。

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

先說(shuō)結(jié)論:在執(zhí)行l(wèi)dc指令時(shí),該指令表示int、float或String型常量從常量池推送至棧頂

JVM規(guī)范里Class文件的常量池項(xiàng)的類(lèi)型,有兩種東西(這段內(nèi)容建議配合看書(shū)上168頁(yè)內(nèi)容):

  • CONSTANT_Utf8_info
  • CONSTANT_String_info

??在HotSpot VM中,運(yùn)行時(shí)常量池里,CONSTANT_Utf8_info可以表示Class文件的方法、字段等等,其結(jié)構(gòu)如下:

CONSTANT_Utf8_info結(jié)構(gòu).png

首先是1個(gè)字節(jié)的tag,表示這是一個(gè)CONSTANT_Utf8_info結(jié)構(gòu)的常量,然后是兩個(gè)字節(jié)的length,表示要儲(chǔ)存字節(jié)的長(zhǎng)度,之后是一個(gè)字節(jié)的byte數(shù)組,表示真正的儲(chǔ)存的length個(gè)長(zhǎng)度的字符串。這里需要注意的是,一個(gè)字節(jié)只是代表這里有一個(gè)byte類(lèi)型的數(shù)組,而這個(gè)數(shù)組的長(zhǎng)度當(dāng)然可以遠(yuǎn)遠(yuǎn)大于一個(gè)字節(jié)。當(dāng)然,由于CONSTANT_Utf8_info結(jié)構(gòu)只能用u2即兩個(gè)字節(jié)來(lái)表示長(zhǎng)度,因此長(zhǎng)度的最大值為2byte,也就是65535(注意這跟Android中dex字節(jié)碼65535方法數(shù)限制沒(méi)有什么關(guān)系,但是道理是一樣的).

??后者CONSTANT_String_info是String常量的類(lèi)型,但它并不直接持有String常量的內(nèi)容,而是只持有一個(gè)index,這個(gè)index所指定的另一個(gè)常量池項(xiàng)必須是一個(gè)CONSTANT_Utf8類(lèi)型的常量,這里才真正持有字符串的內(nèi)容。

![Uploading 提綱_620529.png . . .]

??CONSTANT_Utf8會(huì)在類(lèi)加載的過(guò)程中就全部創(chuàng)建出來(lái),而CONSTANT_String則是lazy resolve的,在第一次引用該項(xiàng)的ldc指令被第一次執(zhí)行到的時(shí)候才會(huì)resolve。在尚未resolve的時(shí)候,HotSpot VM把它的類(lèi)型叫做JVM_CONSTANT_UnresolvedString,內(nèi)容跟Class文件里一樣只是一個(gè)index;等到resolve過(guò)后這個(gè)項(xiàng)的常量類(lèi)型就會(huì)變成最終的JVM_CONSTANT_String,
??也就是說(shuō),就HotSpot VM的實(shí)現(xiàn)來(lái)說(shuō),加載類(lèi)的時(shí)候,那些字符串字面量會(huì)進(jìn)入到當(dāng)前類(lèi)的運(yùn)行時(shí)常量池,不會(huì)進(jìn)入全局的字符串常量池(即在StringTable中并沒(méi)有相應(yīng)的引用,在堆中也沒(méi)有對(duì)應(yīng)的對(duì)象產(chǎn)生),在執(zhí)行l(wèi)dc指令時(shí),觸發(fā)lazy resolution這個(gè)動(dòng)作:
??ldc字節(jié)碼在這里的執(zhí)行語(yǔ)義是:到當(dāng)前類(lèi)的運(yùn)行時(shí)常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找該index對(duì)應(yīng)的項(xiàng),如果該項(xiàng)尚未resolve則resolve之,并返回resolve后的內(nèi)容。
??在遇到String類(lèi)型常量時(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的引用。

4.String.intern()用法

String.intern()官方給的定義:

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

實(shí)際上,就是去拿String的內(nèi)容去Stringtable里查表,如果存在,則返回引用,不存在,就把該對(duì)象的"引用"存在Stringtable表里。

這里采用《深入理解Java虛擬機(jī)》書(shū)上的兩個(gè)例子來(lái)解釋這個(gè)問(wèn)題,第一個(gè)例子在P57頁(yè):

public class RuntimeConstantPoolOOM{
    public static void main(String[] args) {

         String str1 = new StringBuilder("計(jì)算機(jī)").append("軟件").toString();
         System.out.println(str1.intern() == str1);

         String str2 = new StringBuilder("ja").append("va").toString();
         System.out.println(str2.intern() == str2);

    }
}

以上代碼,在 JDK6 下執(zhí)行結(jié)果為 false、false,在 JDK7 以上執(zhí)行結(jié)果為 true、false。

??首先我們調(diào)用StringBuilder創(chuàng)建了一個(gè)"計(jì)算機(jī)軟件"String對(duì)象,因?yàn)檎{(diào)用了new關(guān)鍵字,因此是在運(yùn)行時(shí)創(chuàng)建,之前JVM中是沒(méi)有這個(gè)字符串的。
??在 JDK6 下,intern()會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代中,返回的也是這個(gè)永久代中字符串實(shí)例的引用;而在JDK1.7開(kāi)始,intern()方法不在復(fù)制字符串實(shí)例,tring 的 intern 方法首先將嘗試在常量池中查找該對(duì)象的引用,如果找到則直接返回該對(duì)象在常量池中的引用地址

??因此在1.7中,“計(jì)算機(jī)軟件”這個(gè)字符串實(shí)例只存在一份,存在于java堆中!通過(guò)3中的分析,我們知道當(dāng)String str1 = new StringBuilder("計(jì)算機(jī)").append("軟件").toString();這句代碼執(zhí)行完之后,已經(jīng)在堆中創(chuàng)建了一個(gè)字符串對(duì)象,并且在全局字符串常量池中保留了這個(gè)字符串的引用,那么str1.intern()直接返回這個(gè)引用,這當(dāng)然滿(mǎn)足str1.intern() == str1——都是他自己嘛;對(duì)于引用str2,因?yàn)镴VM中已經(jīng)有“java”這個(gè)字符串了,因此new StringBuilder("ja").append("va").toString()會(huì)重新創(chuàng)建一個(gè)新的“java”字符串對(duì)象,而intern()會(huì)返回首次遇到的常量的實(shí)例引用,因此他返回的是系統(tǒng)中的那個(gè)"java"字符串對(duì)象引用(首次),因此會(huì)返回false

??在 JDK6 下 str1、str2 指向的是新創(chuàng)建的對(duì)象,該對(duì)象將在 Java Heap 中創(chuàng)建,所以 str1、str2 指向的是 Java Heap 中的內(nèi)存地址;調(diào)用 intern 方法后將嘗試在常量池中查找該對(duì)象,沒(méi)找到后將其放入常量池并返回,所以此時(shí) str1/str2.intern() 指向的是常量池中的地址,JDK6常量池在永久代,與堆隔離,所以 s1.intern()和s1 的地址當(dāng)然不同了。

第二個(gè)例子在P56頁(yè):

public class Test2 {
    public static void main(String[] args) {
        /**
         * 首先設(shè)置 持久代最大和最小內(nèi)存占用(限定為10M)
         * VM args: -XX:PermSize=10M -XX:MaxPremSize=10M
         */

        List<String> list  = new ArrayList<String>();

        // 無(wú)限循環(huán) 使用 list 對(duì)其引用保證 不被GC  intern 方法保證其加入到常量池中
        int i = 0;
        while (true) {
            // 此處永久執(zhí)行,最多就是將整個(gè) int 范圍轉(zhuǎn)化成字符串并放入常量池
            list.add(String.valueOf(i++).intern());
        }
    }
}

以上代碼在 JDK6 下會(huì)出現(xiàn) Perm 內(nèi)存溢出,JDK7 or high 則沒(méi)問(wèn)題。

??JDK6 常量池存在持久代(不經(jīng)心CG),設(shè)置了持久代大小后,不斷while循環(huán)必將撐滿(mǎn) Perm 導(dǎo)致內(nèi)存溢出;JDK7 常量池被移動(dòng)到 Native Heap(Java Heap,HotSpot VM中不區(qū)分native堆和Java堆),所以即使設(shè)置了持久代大小,也不會(huì)對(duì)常量池產(chǎn)生影響;不斷while循環(huán)在當(dāng)前的代碼中,所有int的字符串相加還不至于撐滿(mǎn) Heap 區(qū),所以不會(huì)出現(xiàn)異常。

三.JAVA 基本類(lèi)型的封裝類(lèi)及對(duì)應(yīng)常量池

??java中基本類(lèi)型的包裝類(lèi)的大部分都實(shí)現(xiàn)了常量池技術(shù),這些類(lèi)是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點(diǎn)數(shù)類(lèi)型的包裝類(lèi)則沒(méi)有實(shí)現(xiàn)。另外上面這5種整型的包裝類(lèi)也只是在對(duì)應(yīng)值小于等于127時(shí)才可使用對(duì)象池,也即對(duì)象不負(fù)責(zé)創(chuàng)建和管理大于127的這些類(lèi)的對(duì)象。

public class StringConstantPool{

    public static void main(String[] args){
        //5種整形的包裝類(lèi)Byte,Short,Integer,Long,Character的對(duì)象,
        //在值小于127時(shí)可以使用常量池
        Integer i1=127;
        Integer i2=127;
        System.out.println(i1==i2);//輸出true

        //值大于127時(shí),不會(huì)從常量池中取對(duì)象
        Integer i3=128;
        Integer i4=128;
        System.out.println(i3==i4);//輸出false
        //Boolean類(lèi)也實(shí)現(xiàn)了常量池技術(shù)

        Boolean bool1=true;
        Boolean bool2=true;
        System.out.println(bool1==bool2);//輸出true

        //浮點(diǎn)類(lèi)型的包裝類(lèi)沒(méi)有實(shí)現(xiàn)常量池技術(shù)
        Double d1=1.0;
        Double d2=1.0;
        System.out.println(d1==d2); //輸出false

    }
}

??在JDK5.0之前是不允許直接將基本數(shù)據(jù)類(lèi)型的數(shù)據(jù)直接賦值給其對(duì)應(yīng)地包裝類(lèi)的,如:Integer i = 5; 但是在JDK5.0中支持這種寫(xiě)法,因?yàn)榫幾g器會(huì)自動(dòng)將上面的代碼轉(zhuǎn)換成如下代碼:Integer i=Integer.valueOf(5);這就是Java的裝箱.JDK5.0也提供了自動(dòng)拆箱:Integer i =5; int j = i;

??以及,這里常量池中緩存的是包裝類(lèi)對(duì)象,而不是基本數(shù)據(jù)類(lèi)型,要注意!??!

最后編輯于
?著作權(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)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類(lèi)相關(guān)的語(yǔ)法,內(nèi)部類(lèi)的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線(xiàn)程的語(yǔ)...
    子非魚(yú)_t_閱讀 34,692評(píng)論 18 399
  • 這篇文章是我之前翻閱了不少的書(shū)籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 16,051評(píng)論 3 83
  • 轉(zhuǎn)自:http://blog.csdn.net/jackfrued/article/details/4492194...
    王帥199207閱讀 8,805評(píng)論 3 93
  • 今天是我在簡(jiǎn)書(shū)第二天,發(fā)生了一個(gè)大新聞。 鹿晗和關(guān)曉彤公開(kāi)了!
    王淑彤閱讀 266評(píng)論 0 0
  • “千年之后的你會(huì)在哪里——” “喂?嗯,我這邊就要結(jié)束了,嗯,晚上回家吃飯?!蓖跽駫焐想娫?huà),貼上了最后一個(gè)...
    九月小麥田閱讀 253評(píng)論 0 0

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