徹底搞清楚class常量池、運行時常量池、字元串常量池
徹底搞清楚class常量池、運行時常量池、字元串常量池
常量池-靜態常量池
也叫 class文件常量池,主要存放編譯期生成的各種字面量(Literal)和符號引用(Symbolic References)。
- 字面量:例如文本字元串、fina修飾的常量。
int b = 2;
int c = "abcdefg";
- 符號引用:例如類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符
// 第3部分,常量池資訊
Constant pool:
常量池-運行時常量池
- 當類載入到記憶體中後,JVM就會將class常量池中的內容存放到運行時常量池中;運行時常量池裡面存儲的主要是編譯期間生成的字面量、符號引用等等。
- 類載入在鏈接環節的解析過程,會符號引用轉換成直接引用(靜態鏈接)。此處得到的直接引用也是放到運行時常量池中的。
- 運行期間可以動態放入新的常量。
常量池-字元串常量池
字元串常量池,也可以理解成運行時常量池分出來的一部分。類載入到記憶體的時候,字元串會存到字元串常量池裡面。利用池的概念,避免大量頻繁創建字元串。
- JDK6時字元串常量池位於運行時常量池,JDK7挪到堆中。
Hotspot8之前,使用持久代實現方法區,由於持久代記憶體不好估算,很容易到值OOM:Perm Gen異常。而元空間是本地記憶體,取決於作業系統分配記憶體。
字元串常量池位置變遷
Jdk1.6及之前: 有永久代, 運行時常量池在永久代,運行時常量池包含字元串常量池
Jdk1.7:有永久代,但已經逐步「去永久代」,字元串常量池從永久代里的運行時常量池分離到堆里
Jdk1.8及之後: 無永久代,運行時常量池在元空間,字元串常量池裡依然在堆里
創建字元串操作
- 字面量賦值
String s = "lzp";
創建字元串對象,存放到字元串常量池中。s指向常量池中對象引用。
- new String對象
String c = new String("lzp");
new 新字元串對象,會在堆和字元串常量池中都創建對象。
- intern方法
String中的intern方法是一個 native 的方法,當調用 intern方法時,如果池已經包含一個等於此String對象的字元串(用equals(oject)方法確定),則返回池中的字元串。否則,返回堆中String對象的引用(jdk1.6是將 堆中的String對象 複製到字元串常量池,再返回常量池中的引用)。
String c = new String("lzp");
String d = c.intern();
System.out.println(c == d); // false
c指向堆對象,d指向常量池對象,因此必然不相等。
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2); // true
// 在 JDK 1.6 下輸出是 false,創建了 6 個對象
JDK7以後會創建2個字元串常量池對象「he","llo"
,new 3個堆對象」he","llo","hello"
,字元串常量池沒有hello對象引用。調s1的intern方法,hello指向new出來的hello對象。因此JDK7版本創建了5個對象。s1調intern()方法,返回堆中對象引用。
當然,很多部落格中也說字元串常量池中保存的是堆對象的引用,即堆中有5個對象2個he,2個llo,1個hello
。字元串常量池底層是hotspot的C++實現的,底層類似一個 HashTable, 保存的本質上是字元串對象的引用。
眾說紛紜,不好確定。但是兩種情況的外在表現是一致的,字元串字面量對象在常量池中。
編譯期優化
String a = "awecoder";
String b = "awe" + "coder";
System.out.println(a == b); // true
// 下面的也可以優化
"a" + 1 == "a1"
"a" + 3.4 = "a3.4"
b也是字面量,由於”awe”和”coder”在編譯期已確定,JVM編譯期將其優化為一個字元串字面量。
String a = "awecoder";
String b = "awe";
final String finalb = "awe";
System.out.println(a == b + "coder"); // false
System.out.println(a == finalb + "coder"); // true
編譯期確定不了,例如new對象便不能優化。對於連接符”+”周圍是否有變數,能夠優化還是取決於變數是否確定。兩者底層實現不同,一個是編譯期優化成一個字面量,另一個底層使用StringBuilder的append()方法實現(反編譯位元組碼文件可以觀察到)。