柚子快報(bào)激活碼778899分享:java JVM常量池詳解
柚子快報(bào)激活碼778899分享:java JVM常量池詳解
歡迎大家關(guān)注我的微信公眾號(hào):
目錄
Class常量池與運(yùn)行時(shí)常量池?
字符串常量池?
字符串常量池的設(shè)計(jì)思想?
?三種字符串操作(Jdk1.7 及以上版本)
字符串常量池位置?
字符串常量池設(shè)計(jì)原理
String常量池問(wèn)題的幾個(gè)例子?
八種基本類型的包裝類和對(duì)象池
Class常量池與運(yùn)行時(shí)常量池?
????????Class常量池可以理解為是Class文件中的資源倉(cāng)庫(kù)。 Class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息就是常量池(constant pool table),用于存放編譯期生成的各種字面量(Literal)和符號(hào)引用(Symbolic References)。?
一個(gè)class文件的16進(jìn)制大體結(jié)構(gòu)如下圖:?
?對(duì)應(yīng)的含義如下,細(xì)節(jié)可以查下oracle官方文檔:
????????當(dāng)然我們一般不會(huì)去人工解析這種16進(jìn)制的字節(jié)碼文件,我們一般可以通過(guò)javap命令生成更可讀的JVM字節(jié)碼指令文件:
javap -v Math.class
?紅框標(biāo)出的就是class常量池信息,常量池中主要存放兩大類常量:字面量和符號(hào)引用。
字面量
????????字面量就是指由字母、數(shù)字等構(gòu)成的字符串或者數(shù)值常量
????????字面量只可以右值出現(xiàn),所謂右值是指等號(hào)右邊的值,如:int a=1 這里的a為左值,1為右值。在這個(gè)例子中1就是字面量。
int a = 1;
int b = 2;
int c = "abcdefg";
int d = "abcdefg";
符號(hào)引用
????????符號(hào)引用是編譯原理中的概念,是相對(duì)于直接引用來(lái)說(shuō)的。主要包括了以下三類常量:
類和接口的全限定名?字段的名稱和描述符?方法的名稱和描述符
????????上面的a,b就是字段名稱,就是一種符號(hào)引用,還有Math類常量池里的 Lcom/tuling/jvm/Math 是類的全限定名,main和compute是方法名稱,()是一種UTF8格式的描述符,這些都是符號(hào)引用。
????????這些常量池現(xiàn)在是靜態(tài)信息,只有到運(yùn)行時(shí)被加載到內(nèi)存后,這些符號(hào)才有對(duì)應(yīng)的內(nèi)存地址信息,這些常量池一旦被裝入內(nèi)存就變成運(yùn)行時(shí)常量池,對(duì)應(yīng)的符號(hào)引用在程序加載或運(yùn)行時(shí)會(huì)被轉(zhuǎn)變?yōu)楸患虞d到內(nèi)存區(qū)域的代碼的直接引用,也就是我們說(shuō)的動(dòng)態(tài)鏈接了。例如,compute()這個(gè)符號(hào)引用在運(yùn)行時(shí)就會(huì)被轉(zhuǎn)變?yōu)閏ompute()方法具體代碼在內(nèi)存中的地址,主要通過(guò)對(duì)象頭里的類型指針去轉(zhuǎn)換直接引用。
字符串常量池?
字符串常量池的設(shè)計(jì)思想?
????????字符串的分配,和其他的對(duì)象分配一樣,耗費(fèi)高昂的時(shí)間與空間代價(jià),作為最基礎(chǔ)的數(shù)據(jù)類型,大量頻繁的創(chuàng)建字符串,極大程度地影響程序的性能
????????JVM為了提高性能和減少內(nèi)存開(kāi)銷,在實(shí)例化字符串常量的時(shí)候進(jìn)行了一些優(yōu)化
為字符串開(kāi)辟一個(gè)字符串常量池,類似于緩存區(qū)創(chuàng)建字符串常量時(shí),首先查詢字符串常量池是否存在該字符串存在該字符串,返回引用實(shí)例,不存在,實(shí)例化該字符串并放入池中
?三種字符串操作(Jdk1.7 及以上版本)
直接賦值字符串
String s = "zhuge"; // s指向常量池中的引用
????????這種方式創(chuàng)建的字符串對(duì)象,只會(huì)在常量池中。
????????因?yàn)橛?zhuge"這個(gè)字面量,創(chuàng)建對(duì)象s的時(shí)候,JVM會(huì)先去常量池中通過(guò) equals(key) 方法,判斷是否有相同的對(duì)象。如果有,則直接返回該對(duì)象在常量池中的引用;如果沒(méi)有,則會(huì)在常量池中創(chuàng)建一個(gè)新對(duì)象,再返回引用。
new String();
String s1 = new String("zhuge"); // s1指向內(nèi)存中的對(duì)象引用
????????這種方式會(huì)保證字符串常量池和堆中都有這個(gè)對(duì)象,沒(méi)有就創(chuàng)建,最后返回堆內(nèi)存中的對(duì)象引用。
????????因?yàn)橛?zhuge"這個(gè)字面量,所以會(huì)先檢查字符串常量池中是否存在字符串"zhuge"。不存在,先在字符串常量池里創(chuàng)建一個(gè)字符串對(duì)象;再去內(nèi)存中創(chuàng)建一個(gè)字符串對(duì)象"zhuge";存在的話,就直接去堆內(nèi)存中創(chuàng)建一個(gè)字符串對(duì)象"zhuge";最后,將內(nèi)存中的引用返回。
intern方法
String s1 =?new?String("zhuge");
String s2 = s1.intern();
System.out.println(s1 == s2); //false
????????String中的intern方法是一個(gè) native 的方法,當(dāng)調(diào)用 intern方法時(shí),如果池已經(jīng)包含一個(gè)等于此String對(duì)象的字符串(用equals(oject)方法確定),則返回池中的字符串。否則,將intern返回的引用指向當(dāng)前字符串 s1(jdk1.6版本需要將 s1 復(fù)制到字符串常量池里)。?
字符串常量池位置?
Jdk1.6及之前: 有永久代, 運(yùn)行時(shí)常量池在永久代,運(yùn)行時(shí)常量池包含字符串常量池
Jdk1.7:有永久代,但已經(jīng)逐步“去永久代”,字符串常量池從永久代里的運(yùn)行時(shí)常量池分離到堆里
Jdk1.8及之后: 無(wú)永久代,運(yùn)行時(shí)常量池在元空間,字符串常量池里依然在堆里
用一個(gè)程序證明下字符串常量池在哪里(通過(guò)內(nèi)存溢出報(bào)錯(cuò)信息可知):
/**
* jdk6:-Xms6M -Xmx6M -XX:PermSize=6M -XX:MaxPermSize=6M
* jdk8:-Xms6M -Xmx6M -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
*/
public class RuntimeConstantPoolOOM{
public static void main(String[] args) {
ArrayList
for (int i = 0; i < 10000000; i++) {
String str = String.valueOf(i).intern();
list.add(str);
}
}
}
運(yùn)行結(jié)果:
jdk7及以上:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
jdk6:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
字符串常量池設(shè)計(jì)原理
????????字符串常量池底層是hotspot的C++實(shí)現(xiàn)的,底層類似一個(gè) HashTable, 保存的本質(zhì)上是字符串對(duì)象的引用。
看一道比較常見(jiàn)的面試題,下面的代碼創(chuàng)建了多少個(gè) String 對(duì)象?
String s1 =?new?String("he") +?new?String("llo");
String s2 = s1.intern();
?
System.out.println(s1 == s2);
// 在 JDK 1.6 下輸出是 false,創(chuàng)建了 6 個(gè)對(duì)象
// 在 JDK 1.7 及以上的版本輸出是 true,創(chuàng)建了 5 個(gè)對(duì)象
// 當(dāng)然我們這里沒(méi)有考慮GC,但這些對(duì)象確實(shí)存在或存在過(guò)
????????為什么輸出會(huì)有這些變化呢?主要還是字符串池從永久代中脫離、移入堆區(qū)的原因, intern() 方法也相應(yīng)發(fā)生了變化:
????????在 JDK 1.6 中,調(diào)用 intern() 首先會(huì)在字符串池中尋找 equal() 相等的字符串,假如字符串存在就返回該字符串在字符串池中的引用;假如字符串不存在,虛擬機(jī)會(huì)重新在永久代上創(chuàng)建一個(gè)實(shí)例,將 StringTable 的一個(gè)表項(xiàng)指向這個(gè)新創(chuàng)建的實(shí)例。
????????在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的對(duì)象。字符串存在時(shí)和 JDK 1.6一樣,但是字符串不存在時(shí)不再需要重新創(chuàng)建實(shí)例,可以直接指向堆上的實(shí)例。
????????由上面兩個(gè)圖,也不難理解為什么 JDK 1.6 字符串池溢出會(huì)拋出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 及以上版本拋出 OutOfMemoryError: Java heap space 。
String常量池問(wèn)題的幾個(gè)例子?
示例1:
String s0 = "zhuge";
String s1 = "zhuge";
String s2 = "zhu" + "ge";
System.out.println( s0 == s1 ); //true
System.out.println( s0 == s2 ); //true
????????分析:因?yàn)槔又械?s0和s1中的”zhuge”都是字符串常量,它們?cè)诰幾g期就被確定了,所以s0==s1為true;而”zhu”和”ge”也都是字符串常量,當(dāng)一個(gè)字符串由多個(gè)字符串常量連接而成時(shí),它自己肯定也是字符串常量,所以s2也同樣在編譯期就被優(yōu)化為一個(gè)字符串常量"zhuge",所以s2也是常量池中” zhuge”的一個(gè)引用。所以我們得出s0==s1==s2。
示例2:?
String s0 = "zhuge";
String s1 = new String("zhuge");
String s2 = "zhu" + new String("ge");
System.out.println( s0 == s1 ); // false
System.out.println( s0 == s2 ); // false
System.out.println( s1 == s2 ); // false
????????分析:用new String() 創(chuàng)建的字符串不是常量,不能在編譯期就確定,所以new String() 創(chuàng)建的字符串不放入常量池中,它們有自己的地址空間。
????????s0還是常量池 中"zhuge”的引用,s1因?yàn)闊o(wú)法在編譯期確定,所以是運(yùn)行時(shí)創(chuàng)建的新對(duì)象”zhuge”的引用,s2因?yàn)橛泻蟀氩糠?new String(”ge”)所以也無(wú)法在編譯期確定,所以也是一個(gè)新創(chuàng)建對(duì)象”zhuge”的引用;明白了這些也就知道為何得出此結(jié)果了。
示例3:?
String a = "a1";
String b = "a" + 1;
System.out.println(a == b); // true
String a = "atrue";
String b = "a" + "true";
System.out.println(a == b); // true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println(a == b); // true
????????分析:JVM對(duì)于字符串常量的"+"號(hào)連接,將在程序編譯期,JVM就將常量字符串的"+"連接優(yōu)化為連接后的值,拿"a" + 1來(lái)說(shuō),經(jīng)編譯器優(yōu)化后在class中就已經(jīng)是a1。在編譯期其字符串常量的值就確定下來(lái),故上面程序最終的結(jié)果都為true。
示例4:?
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // false
????????分析:JVM對(duì)于字符串引用,由于在字符串的"+"連接中,有字符串引用存在,而引用的值在程序編譯期是無(wú)法確定的,即"a" + bb無(wú)法被編譯器優(yōu)化,只有在程序運(yùn)行期來(lái)動(dòng)態(tài)分配并將連接后的新地址賦給b。所以上面程序的結(jié)果也就為false。
示例5:
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // true
????????分析:和示例4中唯一不同的是bb字符串加了final修飾,對(duì)于final修飾的變量,它在編譯時(shí)被解析為常量值的一個(gè)本地拷貝存儲(chǔ)到自己的常量池中或嵌入到它的字節(jié)碼流中。所以此時(shí)的"a" + bb和"a" + "b"效果是一樣的。故上面程序的結(jié)果為true。
示例6:
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println(a == b); // false
private static String getBB() {
return "b";
}
????????分析:JVM對(duì)于字符串引用bb,它的值在編譯期無(wú)法確定,只有在程序運(yùn)行期調(diào)用方法后,將方法的返回值和"a"來(lái)動(dòng)態(tài)連接并分配地址為b,故上面 程序的結(jié)果為false。
關(guān)于String是不可變的
?????? 通過(guò)上面例子可以得出得知:
String? s? =? "a" + "b" + "c"; //就等價(jià)于String s = "abc";
String? a? =? "a";
String? b? =? "b";
String? c? =? "c";
String? s1? =?? a? +? b? +? c;
?????????s1 這個(gè)就不一樣了,可以通過(guò)觀察其JVM指令碼發(fā)現(xiàn)s1的"+"操作會(huì)變成如下操作:
StringBuilder temp = new StringBuilder();
temp.append(a).append(b).append(c);
String s = temp.toString();
最后再看一個(gè)例子:?
//字符串常量池:"計(jì)算機(jī)"和"技術(shù)" 堆內(nèi)存:str1引用的對(duì)象"計(jì)算機(jī)技術(shù)"
//堆內(nèi)存中還有個(gè)StringBuilder的對(duì)象,但是會(huì)被gc回收,StringBuilder的toString方法會(huì)new String(),這個(gè)String才是真正返回的對(duì)象引用
String str2 = new StringBuilder("計(jì)算機(jī)").append("技術(shù)").toString();
//沒(méi)有出現(xiàn)"計(jì)算機(jī)技術(shù)"字面量,所以不會(huì)在常量池里生成"計(jì)算機(jī)技術(shù)"對(duì)象
System.out.println(str2 == str2.intern()); //true
//"計(jì)算機(jī)技術(shù)" 在池中沒(méi)有,但是在heap中存在,則intern時(shí),會(huì)直接返回該heap中的引用
//字符串常量池:"ja"和"va" 堆內(nèi)存:str1引用的對(duì)象"java"
//堆內(nèi)存中還有個(gè)StringBuilder的對(duì)象,但是會(huì)被gc回收,StringBuilder的toString方法會(huì)new String(),這個(gè)String才是真正返回的對(duì)象引用
String str1 = new StringBuilder("ja").append("va").toString();
//沒(méi)有出現(xiàn)"java"字面量,所以不會(huì)在常量池里生成"java"對(duì)象
System.out.println(str1 == str1.intern()); //false
//之所以為false,是因?yàn)閖ava是關(guān)鍵字,在JVM初始化的相關(guān)類里肯定早就放進(jìn)字符串常量池了
String s1=new String("test");
System.out.println(s1==s1.intern()); //false
//"test"作為字面量,放入了池中,而new時(shí)s1指向的是heap中新生成的string對(duì)象,s1.intern()指向的是"test"字面量之前在池中生成的字符串對(duì)象
String s2=new StringBuilder("abc").toString();
System.out.println(s2==s2.intern()); //false
//同上
八種基本類型的包裝類和對(duì)象池
????????java中基本類型的包裝類的大部分都實(shí)現(xiàn)了常量池技術(shù)(嚴(yán)格來(lái)說(shuō)應(yīng)該叫對(duì)象池,在堆上),這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點(diǎn)數(shù)類型的包裝類則沒(méi)有實(shí)現(xiàn)。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對(duì)應(yīng)值小于等于127時(shí)才可使用對(duì)象池,也即對(duì)象不負(fù)責(zé)創(chuàng)建和管理大于127的這些類的對(duì)象。因?yàn)橐话氵@種比較小的數(shù)用到的概率相對(duì)較大。
public class Test {
public static void main(String[] args) {
//5種整形的包裝類Byte,Short,Integer,Long,Character的對(duì)象,
//在值小于127時(shí)可以使用對(duì)象池
//這種調(diào)用底層實(shí)際是執(zhí)行的Integer.valueOf(127),里面用到了IntegerCache對(duì)象池
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);//輸出true
//值大于127時(shí),不會(huì)從對(duì)象池中取對(duì)象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//輸出false
//用new關(guān)鍵詞新生成對(duì)象不會(huì)使用對(duì)象池
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6);//輸出false
//Boolean類也實(shí)現(xiàn)了對(duì)象池技術(shù)
Boolean bool1 = true;
Boolean bool2 = true;
System.out.println(bool1 == bool2);//輸出true
//浮點(diǎn)類型的包裝類沒(méi)有實(shí)現(xiàn)對(duì)象池技術(shù)
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2);//輸出false
}
}
?
?
?
?
?
柚子快報(bào)激活碼778899分享:java JVM常量池詳解
推薦閱讀
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。