
??需要說(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ù)的值,也就是abc和0x101(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ū)的結(jié)構(gòu),但是還有不完整的地方,為了說(shuō)明全局字符串常量池概念,就必須拿出下面這張圖:

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

??其中的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)如下:

首先是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)容。

??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)型,要注意!??!