一莺奸、概述
- 常量池:編譯期被確定佛呻,*.class文件中的一部分,包含字面量(Literal)和符號(hào)引用(Symbolic Reference)匪蝙。
- 字面量:文本字符串、聲明為final的常量值(int/long/double...)等习贫。
- 符號(hào)引用:類和接口的完全限定名(Fully Qualified Name)骗污、字段的名稱和描述符(Descriptor)、方法的名稱和描述符沈条。
- 運(yùn)行時(shí)常量池:方法區(qū)的一部分需忿,jvm在完成類裝載操作后,將class文件中的常量池載入內(nèi)存并保存在方法區(qū)中蜡歹。
- JDK1.6之前字符串常量池位于方法區(qū)屋厘。
JDK1.7字符串常量池已經(jīng)被移至堆。
JDK1.8字符串常量池位移至元空間月而。
二汗洒、字符串常量池
字符串常量池屬于運(yùn)行時(shí)常量池的一部分
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
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
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
java中==比較的是內(nèi)存地址
- s1 == s2 (true),s1父款、s2賦值時(shí)均使用的字符串字面量"Hello"溢谤,在編譯期間,這種字面量會(huì)直接放入class文件的常量池中憨攒,載入運(yùn)行時(shí)常量池后世杀,s1、s2指向的是同一個(gè)內(nèi)存地址肝集。
- s1 == s3 (true)瞻坝,s3雖然是動(dòng)態(tài)拼接出來(lái)的字符串,但所有參與拼接的部分都是已知的字面量杏瞻,在編譯期間所刀,這種拼接會(huì)被優(yōu)化衙荐,因此String s3 = "Hel" + "lo";在class文件中被優(yōu)化成String s3 = "Hello";,所以s1 == s3成立浮创。
-
s1 == s4 (false)忧吟,s4雖然也是拼接出來(lái)的,但new String("lo")這部分不是已知字面量斩披,編譯器不會(huì)優(yōu)化瀑罗,必須等到運(yùn)行時(shí)才可以確定結(jié)果,所以s4指向堆中某個(gè)地址雏掠。
-
s1 == s9 (false),s9是s7劣像、s8兩個(gè)變量拼接乡话,都是不可預(yù)料的,編譯器不作優(yōu)化耳奕,運(yùn)行時(shí)拼接成新字符串存于堆中某個(gè)地址绑青。
- s4 == s5 (false),二者都在堆中屋群,但地址不同闸婴。
- s1 == s6 (true),這兩個(gè)相等完全歸功于intern()方法芍躏,s5在堆中邪乍,內(nèi)容為"Hello" ,intern方法會(huì)嘗試將"Hello"字符串添加到常量池中对竣,并返回其在常量池中的地址庇楞,因?yàn)槌A砍刂幸呀?jīng)有了"Hello"字符串,所以intern方法直接返回地址否纬;而s1在編譯期就已經(jīng)指向常量池了吕晌,因此s1和s6指向同一地址,相等临燃。
- 以上所講僅涉及字符串常量池睛驳,實(shí)際上還有整型常量池、浮點(diǎn)型常量池等等膜廊,但都大同小異
- 數(shù)值類型的常量池不可以手動(dòng)添加常量乏沸,程序啟動(dòng)時(shí)常量池中的常量就已經(jīng)確定了,比如整型常量池中的常量范圍:-128~127爪瓜,只有這個(gè)范圍的數(shù)字可以用到常量池屎蜓。
三個(gè)非常重要的結(jié)論:
- 必須要關(guān)注編譯期的行為,才能更好的理解常量池钥勋。
- 運(yùn)行時(shí)常量池中的常量炬转,基本來(lái)源于各個(gè)class文件中的常量池辆苔。
- 程序運(yùn)行時(shí),除非手動(dòng)向常量池中添加常量(比如調(diào)用intern方法)扼劈,否則jvm不會(huì)自動(dòng)添加常量到常量池驻啤。
三、常量池溢出
/**
* jdk1.6 -XX:MaxPermSize=5M OutOfMemoryError: PermGen space
* jdk1.7 -Xmx5M OutOfMemoryError: Java heap space
* jdk1.8 -Xmx5M OutOfMemoryError: GC overhead limit exceeded
* jdk1.8 -XX:MaxMetaspaceSize=2M OutOfMemoryError: Metaspace
*/
public class ConstantPoolOOM {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int index = 0;
while (true) {
list.add(String.valueOf(index++).intern());
}
}
}
- jdk1.6運(yùn)行時(shí)常量池在方法區(qū)中荐吵,設(shè)置-XX:MaxPermSize=5M骑冗,導(dǎo)致OutOfMemoryError: PermGen space
- jdk1.7運(yùn)行時(shí)常量池在堆中,僅僅保存常量的引用先煎,常量對(duì)象在堆中贼涩,設(shè)置-Xmx5M,導(dǎo)致OutOfMemoryError: Java heap space
- jdk1.8運(yùn)行時(shí)常量池在元空間中薯蝎,僅僅保存常量的引用遥倦,常量對(duì)象在堆中,
設(shè)置-XX:MaxMetaspaceSize=2M占锯,導(dǎo)致OutOfMemoryError: Metaspace
設(shè)置-Xmx5M袒哥,導(dǎo)致OutOfMemoryError: GC overhead limit exceeded
四、jdk1.6和1.7+常量池的差異
/**
* jdk1.6 false false
* jdk1.7+ true false
*/
public class ConstantPoolTest {
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);
}
}
- 上面代碼在jdk1.6中執(zhí)行會(huì)打印兩個(gè)false
1.6中intern()方法會(huì)把首次出現(xiàn)的字符串實(shí)例復(fù)制到運(yùn)行時(shí)常量池(方法區(qū))中消略,并返回方法區(qū)中這個(gè)實(shí)例的地址堡称,而str1 str2的地址都在堆中,所以兩次都打印false艺演。 - 在jdk1.7種執(zhí)行第一個(gè)會(huì)打印true却紧,第二個(gè)會(huì)打印false
1.7以后版本intern()方法會(huì)把首次出現(xiàn)的字符串實(shí)例的引用運(yùn)行時(shí)常量池中,
"計(jì)算機(jī)軟件"是首次出現(xiàn)的字符串胎撤,會(huì)把str1的引用存入運(yùn)行時(shí)常量池啄寡,所以str1.intern()返回的就是str1的引用,打印true哩照。
"java"在創(chuàng)建str2對(duì)象之前已出現(xiàn)過(guò)挺物,運(yùn)行時(shí)常量池中已經(jīng)存在,所以打印false飘弧。
參考資料:
http://www.cnblogs.com/iyangyuan/p/4631696.html
https://segmentfault.com/a/1190000010412582