徹底搞清楚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()方法實現(反編譯位元組碼文件可以觀察到)。

Tags: