??需要說明的一點是,這篇文章是以《深入理解Java虛擬機》第二版這本書為基礎(chǔ)的渡蜻,這里假設(shè)大家已經(jīng)了解了JVM的運行時區(qū)域术吝,以及class文件結(jié)構(gòu),類加載流程等基礎(chǔ)內(nèi)容茸苇。當然排苍,文中我們也會提一提相關(guān)的內(nèi)容作為復(fù)習(xí)總結(jié)
一.JVM有幾種常量池
??主要分為:Class文件常量池、運行時常量池学密,當然還有全局字符串常量池淘衙,以及基本類型包裝類對象常量池
1.Class文件常量池
??閱讀過《深入理解Java虛擬機》這本書第6章內(nèi)容的小伙伴肯定知道,class文件是一組以8位字節(jié)為單位的二進制數(shù)據(jù)流腻暮,在java代碼的編譯期間彤守,我們編寫的.java文件就被編譯為.class文件格式的二進制數(shù)據(jù)存放在磁盤中,其中就包括class文件常量池烹卒。
??class 文件中存在常量池(非運行時常量池)吉捶,其在編譯階段就已經(jīng)確定;JVM 規(guī)范對 class 文件結(jié)構(gòu)有著嚴格的規(guī)范,必須符合此規(guī)范的 class 文件才會被 JVM 認可和裝載赴涵。
為了方便說明,我們這里先寫一個很簡單的類:
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;
}
}
通過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
可以看到這個命令之后我們得到了該class文件的版本號砚哗、常量池、已經(jīng)編譯后的字節(jié)碼指令(處于篇幅原因這里省略)蔗坯,下面我們會對照這個class文件來講解:
??這里我們需要說明一下康震,既然是常量池,那么其中個存放的肯定是“常量”宾濒,那么什么是“常量”呢腿短?class文件常量池主要存放兩大常量:字面量和符號引用:
1).字面量
字面量接近于java語言層面的常量概念,主要包括:
-
文本字符串绘梦,也就是我們經(jīng)常聲明的:
public String s = "abc";
中的"abc"
#9 = Utf8 s
#3 = String #31 // abc
#31 = Utf8 abc
- 用final修飾的成員變量橘忱,包括靜態(tài)變量、實例變量和局部變量
#11 = Utf8 f
#12 = Utf8 ConstantValue
#13 = Integer 257
??這里需要說明的一點卸奉,上面說的存在于常量池的字面量钝诚,指的是數(shù)據(jù)的值,也就是abc
和0x101(257)
,通過上面對常量池的觀察可知這兩個字面量是確實存在于常量池的榄棵。
??而對于基本類型數(shù)據(jù)(甚至是方法中的局部變量)凝颇,也就是上面的private int value = 1
;常量池中只保留了他的的字段描述符I
和字段的名稱value
潘拱,他們的字面量不會存在于常量池:
2).符號引用
符號引用主要設(shè)涉及編譯原理方面的概念,包括下面三類常量:
-
類和接口的全限定名拧略,也就是
Ljava/lang/String;
這樣芦岂,將類名中原來的"."替換為"/"得到的,主要用于在運行時解析得到類的直接引用辑鲤,像上面:
#5 = Class #33 // JavaBasicKnowledge/JavaBean
#33 = Utf8 JavaBasicKnowledge/JavaBean
- 字段的名稱和描述符盔腔,字段也就是類或者接口中聲明的變量,包括類級別變量(static)和實例級的變量
#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
//這兩個是局部變量月褥,值保留字段名稱
#23 = Utf8 v
#24 = Utf8 temp
可以看到弛随,class文件的常量池中也存在方法中的局部變量,但是沒有宁赤;但是常量池外面的字段表中不包括局部變量舀透;
- 方法的名稱和描述符,方法的描述類似于JNI動態(tài)注冊時的“方法簽名”决左,也就是參數(shù)類型+返回值類型:
#21 = Utf8 setValue
#22 = Utf8 (I)V
#25 = Utf8 getValue
#26 = Utf8 ()I
2.運行時常量池
??運行時常量池是方法區(qū)的一部分愕够,所以也是全局共享的。我們知道佛猛,jvm在執(zhí)行某個類的時候惑芭,必須經(jīng)過加載、連接(驗證,準備,解析)继找、初始化遂跟,在第一步的加載階段,虛擬機需要完成下面3件事情:
- 通過一個類的“全限定名”來獲取此類的二進制字節(jié)流
- 將這個字節(jié)流所代表的靜態(tài)儲存結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個類代表這類的java.lang.Class對象婴渡,作為方法區(qū)這個類的各種數(shù)據(jù)訪問的入口
??這里需要說明的一點是幻锁,類對象和普通的實例對象是不同的,類對象是在類加載的時候生成的边臼,普通的實例對象一般是在調(diào)用new之后創(chuàng)建哄尔。
??上面第二條,將class字節(jié)流代表的靜態(tài)儲存結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)柠并,其中就包含了class文件常量池進入運行時常量池的過程岭接。這里需要強調(diào)一下,不同的類共用一個運行時常量池(http://blog.csdn.net/fan2012huan/article/details/52759614)臼予,同時在進入運行時常量池的過程中鸣戴,多個class文件中常量池中相同的字符串只會存在一份在運行時常量池中,這也是一種優(yōu)化瘟栖。
??運行時常量池的作用是存儲 Java class文件常量池中的符號信息葵擎。運行時常量池 中保存著一些 class 文件中描述的符號引用,同時在類加載的“解析階段”還會將這些符號引用所翻譯出來的直接引用(直接指向?qū)嵗龑ο蟮闹羔?存儲在 運行時常量池 中半哟。
??運行時常量池相對于 class 常量池一大特征就是其具有動態(tài)性酬滤,Java 規(guī)范并不要求常量只能在運行時才產(chǎn)生签餐,也就是說運行時常量池中的內(nèi)容并不全部來自 class 常量池,class 常量池并非運行時常量池的唯一數(shù)據(jù)輸入口盯串;在運行時可以通過代碼生成常量并將其放入運行時常量池中氯檐,這種特性被用的較多的是String.intern()(這個方法下面將會詳細講)。
二.全局字符串常量池
??字符串常量池單獨列出來說有兩個原因:
- 不同于基本數(shù)據(jù)類型体捏,String類型是一個final對象,他的字面量存在于class文件常量池中冠摄,但是運行期行為卻與普通常量不同
- JDK 1.7中,字符串常量池和類引用被移動到了Java堆中(與運行時常量池分離)几缭,因此不同版本的String行為也有所差異
1.Java中創(chuàng)建字符串對象的兩種方式
??這個問題我想大家一定非常清楚了吧河泳,一般有如下兩種:
String s0 =”hellow”;
String s1=new String (“hellow”);
??第一種我們之前已經(jīng)見過了年栓,這種方式聲明的字面量hellow
是在編譯期就已經(jīng)確定的拆挥,它會直接進入class文件常量池中;當運行期間在全局字符串常量池中會保存它的一個引用某抓,實際上最終還是要在堆上創(chuàng)建一個”hellow”
對象纸兔,這個后面會講。
??第二種方式方式使用了new String()
否副,也就是調(diào)用了String類的構(gòu)造函數(shù)汉矿,我們知道new指令是創(chuàng)建一個類的實例對象并完成加載初始化的,因此這個字符串對象是在運行期才能確定的备禀,創(chuàng)建的字符串對象是在堆內(nèi)存上洲拇。
??因此此時調(diào)用System.out.println(s0 == s1);
返回的肯定是flase,因此==
符號比較的是兩邊元素的地址,s1和s0都存在于堆上痹届,但是地址肯定不相同呻待。
下面我們來看看幾個非常常見的題目:
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
??這個對比第一部分常量池的講解應(yīng)該很好理解打月,因為字面量"Hello"
在運行時會進入運行時常量池(中的字符串常量池队腐,JDK1.7以前),同時同一份字面量只會保留一份奏篙,所有引用都指向這一份字符串柴淘,自然引用的地址也就相同了。
2) s1 == s3
??這個主要牽扯String"+"號編譯器優(yōu)化的問題秘通,s3雖然是動態(tài)拼接出來的字符串为严,但是所有參與拼接的部分都是已知的字面量,在編譯期間肺稀,這種拼接會被優(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
??通過查看編譯后的方法代碼诲锹,可以看到這里加入操作數(shù)棧的ldc指令有兩次,都是“Hello”涉馅,沒有出現(xiàn)“Hel”或者“l(fā)o”,同時這兩個“Hello”指向常量池的通過一個地址归园,都是#2
,因此常量池中也只存在一個“Hello”
字面量稚矿。
3) s1 != s4
??其實這個也不難理解庸诱,但是我們還是先來看看編譯后的字節(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
??我們就不對操作符一一解釋了,可以看到這次確實出現(xiàn)了“String Hel”
和“String lo”
晤揣,原因上面我們也說過桥爽,這是因為new String("lo")
在堆中new了一個String對象出來,而“Hel”
字面量是通過另一種操作在堆中創(chuàng)建的對象昧识,這兩個在堆中不同地方創(chuàng)建的對象是通過StringBuilder.append
方法拼接出來的聚谁,并且最終會調(diào)用StringBuilder.toString
方法輸出(最終輸出的也是“Hello”),這些通過上面字節(jié)碼的分析都可以看得出來,我們來看看StringBuilder.toString
方法:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
??可以看到滞诺,這個最終是拼接出來的一個String對象形导,也就是說,s4指向的一個經(jīng)過StringBuilder拼接之后的String對象习霹,而s1指向的是另一個對象朵耕,這兩個對象的地址當然是不同的了。
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)中的情況是相同的阎曹,都是通過StringBuilder.append拼接后toString輸出的全新對象,至于這個對象被分配到哪里去了煞檩,我們也不知道处嫌。
2.String s1 = "Hello",到底有沒有在堆中創(chuàng)建對象斟湃?
??上面這張圖比是我們通常理解的JVM運行時數(shù)據(jù)區(qū)的結(jié)構(gòu)熏迹,但是還有不完整的地方,為了說明全局字符串常量池概念凝赛,就必須拿出下面這張圖:
這張圖中注暗,可以看到,方法區(qū)實際上是在一塊叫“非堆”的區(qū)域包含——可以簡單粗略的理解為非堆中包含了永生代墓猎,而永生代中又包含了方法區(qū)和字符串常量池捆昏,我們放大一下,一遍大家看的更清楚些:
??其中的Interned String就是全局共享的“字符串常量池(String Pool)”毙沾,和運行時常量池不是一個概念骗卜。但我們在代碼中申明String s1 = "Hello";
這句代碼后,在類加載的過程中,類的class文件的信息會被解析到內(nèi)存的方法區(qū)里寇仓。
??class文件里常量池里大部分數(shù)據(jù)會被加載到“運行時常量池”勇皇,包括String的字面量;但同時“Hello”字符串的一個引用會被存到同樣在“非堆”區(qū)域的“字符串常量池”中焚刺,而"Hello"本體還是和所有對象一樣敛摘,創(chuàng)建在Java堆中。
??當主線程開始創(chuàng)建s1時乳愉,虛擬機會先去字符串池中找是否有equals(“Hello”)的String兄淫,如果相等就把在字符串池中“Hello”的引用復(fù)制給s1;如果找不到相等的字符串蔓姚,就會在堆中新建一個對象捕虽,同時把引用駐留在字符串池,再把引用賦給str坡脐。
??當用字面量賦值的方法創(chuàng)建字符串時泄私,無論創(chuàng)建多少次,只要字符串的值相同备闲,它們所指向的都是堆中的同一個對象晌端。
字符串常量池的本質(zhì)
??看到這里,是時候引出字符串常量池的概念了:字符串常量池是JVM所維護的一個字符串實例的引用表恬砂,在HotSpot VM中咧纠,它是一個叫做StringTable的全局表。在字符串常量池中維護的是字符串實例的引用泻骤,底層C++實現(xiàn)就是一個Hashtable漆羔。這些被維護的引用所指的字符串實例,被稱作”被駐留的字符串”或”interned string”或通常所說的”進入了字符串常量池的字符串”狱掂。
??再強調(diào)一遍:運行時常量池在方法區(qū)(Non-heap)演痒,而JDK1.7后,字符串常量池被移到了heap區(qū)趋惨,因此兩者根本就不是一個概念鸟顺。
3.String"字面量" 是何時進入字符串常量池的?
先說結(jié)論:在執(zhí)行l(wèi)dc指令時,該指令表示int希柿、float或String型常量從常量池推送至棧頂
JVM規(guī)范里Class文件的常量池項的類型诊沪,有兩種東西(這段內(nèi)容建議配合看書上168頁內(nèi)容):
- CONSTANT_Utf8_info
- CONSTANT_String_info
??在HotSpot VM中养筒,運行時常量池里曾撤,CONSTANT_Utf8_info可以表示Class文件的方法、字段等等晕粪,其結(jié)構(gòu)如下:
首先是1個字節(jié)的tag,表示這是一個CONSTANT_Utf8_info結(jié)構(gòu)的常量挤悉,然后是兩個字節(jié)的length,表示要儲存字節(jié)的長度,之后是一個字節(jié)的byte數(shù)組装悲,表示真正的儲存的length個長度的字符串昏鹃。這里需要注意的是,一個字節(jié)只是代表這里有一個byte類型的數(shù)組诀诊,而這個數(shù)組的長度當然可以遠遠大于一個字節(jié)洞渤。當然,由于CONSTANT_Utf8_info結(jié)構(gòu)只能用u2即兩個字節(jié)來表示長度属瓣,因此長度的最大值為2byte载迄,也就是65535(注意這跟Android中dex字節(jié)碼65535方法數(shù)限制沒有什么關(guān)系,但是道理是一樣的).
??后者CONSTANT_String_info是String常量的類型抡蛙,但它并不直接持有String常量的內(nèi)容护昧,而是只持有一個index,這個index所指定的另一個常量池項必須是一個CONSTANT_Utf8類型的常量粗截,這里才真正持有字符串的內(nèi)容惋耙。
??CONSTANT_Utf8會在類加載的過程中就全部創(chuàng)建出來,而CONSTANT_String則是lazy resolve的熊昌,在第一次引用該項的ldc指令被第一次執(zhí)行到的時候才會resolve绽榛。在尚未resolve的時候,HotSpot VM把它的類型叫做JVM_CONSTANT_UnresolvedString婿屹,內(nèi)容跟Class文件里一樣只是一個index蒜田;等到resolve過后這個項的常量類型就會變成最終的JVM_CONSTANT_String,
??也就是說选泻,就HotSpot VM的實現(xiàn)來說冲粤,加載類的時候,那些字符串字面量會進入到當前類的運行時常量池页眯,不會進入全局的字符串常量池(即在StringTable中并沒有相應(yīng)的引用梯捕,在堆中也沒有對應(yīng)的對象產(chǎn)生),在執(zhí)行l(wèi)dc指令時窝撵,觸發(fā)lazy resolution這個動作:
??ldc字節(jié)碼在這里的執(zhí)行語義是:到當前類的運行時常量池(runtime constant pool傀顾,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找該index對應(yīng)的項,如果該項尚未resolve則resolve之碌奉,并返回resolve后的內(nèi)容短曾。
??在遇到String類型常量時,resolve的過程如果發(fā)現(xiàn)StringTable已經(jīng)有了內(nèi)容匹配的java.lang.String的引用赐劣,則直接返回這個引用嫉拐,反之,如果StringTable里尚未有內(nèi)容匹配的String實例的引用魁兼,則會在Java堆里創(chuàng)建一個對應(yīng)內(nèi)容的String對象婉徘,然后在StringTable記錄下這個引用,并返回這個引用出去。
??可見盖呼,ldc指令是否需要創(chuàng)建新的String實例儒鹿,全看在第一次執(zhí)行這一條ldc指令時,StringTable是否已經(jīng)記錄了一個對應(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.
實際上约炎,就是去拿String的內(nèi)容去Stringtable里查表,如果存在蟹瘾,則返回引用章钾,不存在,就把該對象的"引用"存在Stringtable表里热芹。
這里采用《深入理解Java虛擬機》書上的兩個例子來解釋這個問題贱傀,第一個例子在P57頁:
public class RuntimeConstantPoolOOM{
public static void main(String[] args) {
String str1 = new StringBuilder("計算機").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)建了一個"計算機軟件"String對象株搔,因為調(diào)用了new關(guān)鍵字,因此是在運行時創(chuàng)建纯蛾,之前JVM中是沒有這個字符串的纤房。
??在 JDK6 下,intern()會把首次遇到的字符串實例復(fù)制到永久代中翻诉,返回的也是這個永久代中字符串實例的引用炮姨;而在JDK1.7開始,intern()方法不在復(fù)制字符串實例碰煌,tring 的 intern 方法首先將嘗試在常量池中查找該對象的引用舒岸,如果找到則直接返回該對象在常量池中的引用地址
??因此在1.7中,“計算機軟件”這個字符串實例只存在一份芦圾,存在于java堆中蛾派!通過3中的分析,我們知道當String str1 = new StringBuilder("計算機").append("軟件").toString();
這句代碼執(zhí)行完之后个少,已經(jīng)在堆中創(chuàng)建了一個字符串對象洪乍,并且在全局字符串常量池中保留了這個字符串的引用,那么str1.intern()直接返回這個引用夜焦,這當然滿足str1.intern() == str1
——都是他自己嘛壳澳;對于引用str2,因為JVM中已經(jīng)有“java”這個字符串了糊探,因此new StringBuilder("ja").append("va").toString()
會重新創(chuàng)建一個新的“java”字符串對象钾埂,而intern()會返回首次遇到的常量的實例引用河闰,因此他返回的是系統(tǒng)中的那個"java"字符串對象引用(首次)科平,因此會返回false
??在 JDK6 下 str1褥紫、str2 指向的是新創(chuàng)建的對象,該對象將在 Java Heap 中創(chuàng)建瞪慧,所以 str1髓考、str2 指向的是 Java Heap 中的內(nèi)存地址;調(diào)用 intern 方法后將嘗試在常量池中查找該對象弃酌,沒找到后將其放入常量池并返回氨菇,所以此時 str1/str2.intern() 指向的是常量池中的地址,JDK6常量池在永久代妓湘,與堆隔離查蓉,所以 s1.intern()和s1 的地址當然不同了。
第二個例子在P56頁:
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>();
// 無限循環(huán) 使用 list 對其引用保證 不被GC intern 方法保證其加入到常量池中
int i = 0;
while (true) {
// 此處永久執(zhí)行榜贴,最多就是將整個 int 范圍轉(zhuǎn)化成字符串并放入常量池
list.add(String.valueOf(i++).intern());
}
}
}
以上代碼在 JDK6 下會出現(xiàn) Perm 內(nèi)存溢出豌研,JDK7 or high 則沒問題。
??JDK6 常量池存在持久代(不經(jīng)心CG)唬党,設(shè)置了持久代大小后鹃共,不斷while循環(huán)必將撐滿 Perm 導(dǎo)致內(nèi)存溢出;JDK7 常量池被移動到 Native Heap(Java Heap,HotSpot VM中不區(qū)分native堆和Java堆)驶拱,所以即使設(shè)置了持久代大小霜浴,也不會對常量池產(chǎn)生影響;不斷while循環(huán)在當前的代碼中蓝纲,所有int的字符串相加還不至于撐滿 Heap 區(qū)阴孟,所以不會出現(xiàn)異常。
三.JAVA 基本類型的封裝類及對應(yīng)常量池
??java中基本類型的包裝類的大部分都實現(xiàn)了常量池技術(shù)税迷,這些類是Byte,Short,Integer,Long,Character,Boolean
,另外兩種浮點數(shù)類型的包裝類則沒有實現(xiàn)温眉。另外上面這5種整型的包裝類也只是在對應(yīng)值小于等于127時才可使用對象池,也即對象不負責創(chuàng)建和管理大于127的這些類的對象翁狐。
public class StringConstantPool{
public static void main(String[] args){
//5種整形的包裝類Byte,Short,Integer,Long,Character的對象类溢,
//在值小于127時可以使用常量池
Integer i1=127;
Integer i2=127;
System.out.println(i1==i2);//輸出true
//值大于127時,不會從常量池中取對象
Integer i3=128;
Integer i4=128;
System.out.println(i3==i4);//輸出false
//Boolean類也實現(xiàn)了常量池技術(shù)
Boolean bool1=true;
Boolean bool2=true;
System.out.println(bool1==bool2);//輸出true
//浮點類型的包裝類沒有實現(xiàn)常量池技術(shù)
Double d1=1.0;
Double d2=1.0;
System.out.println(d1==d2); //輸出false
}
}
??在JDK5.0之前是不允許直接將基本數(shù)據(jù)類型的數(shù)據(jù)直接賦值給其對應(yīng)地包裝類的露懒,如:Integer i = 5;
但是在JDK5.0中支持這種寫法闯冷,因為編譯器會自動將上面的代碼轉(zhuǎn)換成如下代碼:Integer i=Integer.valueOf(5);
這就是Java的裝箱.JDK5.0也提供了自動拆箱:Integer i =5; int j = i;
??以及,這里常量池中緩存的是包裝類對象懈词,而不是基本數(shù)據(jù)類型蛇耀,要注意!?餐洹纺涤!