前言正文全局字符串池(string pool也有叫作string literal pool)class 文件常量池(class constant pool)運行時常量池(runtime constant pool)三種常量池之間的關聯總結參考連接javascript
Java 的 JVM 的內存可分爲 3 個區:堆內存(heap)、棧內存(stack)和方法區(method)也叫靜態存儲區。java
在學習的過程當中常常還會聽到常量池這一術語,在上節關於數據作 == 比較時,提到了字符串常量池,經查詢得知常量池既不屬於堆,也不屬於棧內存 ,那麼常量池可能就和方法區有所關係,爲此閱讀《深刻淺出JVM》一書,瞭解常量池和方法區的關聯,同時對於常量池的分類也有了必定的認識。web
本文全部代碼都是基於 JDK1.8 進行的。數據結構
在探討常量池的類型以前須要明白什麼是常量。app
在 Java 的內存分配中,總共 3 種常量池:oop
字符串常量池在 Java 內存區域的哪一個位置post
字符串常量池是什麼?性能
在 HotSpot VM 裏實現的 string pool 功能的是一個 StringTable 類,它是一個 Hash 表,默認值大小長度是1009;裏面存的是駐留字符串的引用(而不是駐留字符串實例自身)。也就是說某些普通的字符串實例被這個 StringTable 引用以後就等同被賦予了「駐留字符串」的身份。這個 StringTable 在每一個 HotSpot VM 的實例裏只有一份,被全部的類共享。學習
StringTable 本質上就是個 HashSet<String>
。這是個純運行時的結構,並且是惰性(lazy)維護的。注意它只存儲對java.lang.String 實例的引用,而不存儲 String 對象的內容。 注意,它只存了引用,根據這個引用能夠獲得具體的 String 對象。ui
在 JDK6.0 中,StringTable 的長度是固定的,長度就是 1009,所以若是放入 String Pool 中的 String 很是多,就會形成 hash 衝突,致使鏈表過長,當調用 String#intern() 時會須要到鏈表上一個一個找,從而致使性能大幅度降低;
在 JDK7.0 中,StringTable 的長度能夠經過參數指定:
-XX:StringTableSize=66666
複製代碼
咱們都知道,class 文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用於存放編譯器生成的各類字面量(Literal)和符號引用(Symbolic References)
。 字面量比較接近 Java 語言層面常量的概念,如文本字符串、被聲明爲 final 的常量值等
。 符號引用則屬於編譯原理方面的概念,包括了以下三種類型的常量:
常量池的每一項常量都是一個表,一共有以下表所示的11種各不相同的表結構數據,這每一個表開始的第一位都是一個字節的標誌位(取值1-12),表明當前這個常量屬於哪一種常量類型。
運行時常量池是方法區的一部分。
當 Java 文件被編譯成 class 文件以後,也就是會生成上面所說的 class 常量池,那麼運行時常量池又是何時產生的呢?
JVM 在執行某個類的時候,必須通過加載、鏈接、初始化
,而鏈接又包括驗證、準備、解析(resolve)三個階段。而當類加載到內存中後,JVM 就會將 class 文件常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每一個類都有一個。在上面也說了,class 常量池中存的是字面量和符號引用
,也就是說它們存的並非對象的實例,而是對象的符號引用值。而通過resolve 以後,也就是把符號引用替換爲直接引用,解析的過程會去查詢全局字符串池,也就是上面所說的 StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。
關於 JVM 執行的時候,還涉及到了字符串常量池
。
在類加載階段, JVM 會在堆中建立對應這些 class 文件常量池中的字符串對象實例,並在字符串常量池中駐留其引用。具體在 resolve 階段執行。這些常量全局共享。
複製代碼
這裏說的比較籠統,沒錯,是 resolve 階段,可是並非你們想的那樣,當即就建立對象而且在字符串常量池中駐留了引用。 JVM 規範裏明確指定 resolve 階段能夠是 lazy 的。
JVM 規範裏 Class 文件常量池項的類型,有兩種東西:CONSTANT_Utf8 和CONSTANT_String
。前者是 UTF-8 編碼的字符串類型,後者是 String 常量的類型,但它並不直接持有 String 常量的內容,而是隻持有一個 index,這個 index 所指定的另外一個常量池項必須是一個 CONSTANT_Utf8 類型的常量,這裏才真正持有字符串的內容。
在HotSpot VM中,運行時常量池裏,
CONSTANT_Utf8 -> Symbol*(一個指針,指向一個Symbol類型的C++對象,內容是跟Class文件一樣格式的UTF-8編碼的字符串)
CONSTANT_String -> java.lang.String(一個實際的Java對象的引用,C++類型是oop)
複製代碼
CONSTANT_Utf8 會在類加載的過程當中就所有建立出來,而 CONSTANT_String 則是 lazy resolve
的,例如說在第一次引用該項的 ldc 指令被第一次執行到的時候纔會 resolve。那麼在還沒有 resolve 的時候,HotSpot VM 把它的類型叫作JVM_CONSTANT_UnresolvedString
,內容跟 Class 文件裏同樣只是一個 index;等到 resolve 事後這個項的常量類型就會變成最終的 JVM_CONSTANT_String
,而內容則變成實際的那個 oop。
看到這裏想必也就明白了, 就 HotSpot VM 的實現來講,加載類的時候,那些字符串字面量會進入到當前類的運行時常量池,不會進入全局的字符串常量池(即在 StringTable 中並無相應的引用,在堆中也沒有對應的對象產生)。因此上面提到的,通過 resolve 時,會去查詢全局字符串池,最後把符號引用替換爲直接引用。(即字面量和符號引用雖然在類加載的時候就存入到運行時常量池,可是對於 lazy resolve 的字面量,具體操做仍是會在 resolve 以後進行的。)
關於 lazy resolution 須要在這裏瞭解一下 ldc 指令
簡單地說,它用於將 String 型常量值從常量池中推送至棧頂。
如下面代碼爲例:
public static void main(String[] args) {
String s = "abc";
}
複製代碼
好比說該代碼文件爲 Test.java,首先在文件目錄下打開 Dos 窗口,執行 javac Test.java
進行編譯,而後輸入 javap -verbose Test
查看其編譯後的 class 文件以下:
結合上文所講,在 resolve 階段( constant pool resolution ),字符串字面量被建立對象並在字符串常量池中駐留其引用,可是這個 resolve 是 lazy 的。換句話說並無真正的對象,字符串常量池裏天然也沒有,那麼 ldc 指令還怎麼把值推送至棧頂並進行了賦值操做?或者換一個角度想,既然 resolve 階段是 lazy 的,那總有一個時候它要真正的執行吧,是何時?
執行 ldc 指令就是觸發 lazy resolution 動做的條件
ldc 字節碼在這裏的執行語義是:到當前類的運行時常量池(runtime constant pool,HotSpot VM裏是ConstantPool + ConstantPoolCache)去查找該 index 對應的項,若是該項還沒有 resolve 則 resolve 之,並返回 resolve 後的內容。
在遇到 String 類型常量時,resolve 的過程若是發現 StringTable 已經有了內容匹配的 java.lang.String 的引用,則直接返回這個引用;反之,若是 StringTable 裏還沒有有內容匹配的 String 實例的引用,則會在 Java 堆裏建立一個對應內容的 String 對象,而後在 StringTable 記錄下這個引用,並返回這個引用。
可見,ldc 指令是否須要建立新的 String 實例,全看在第一次執行這一條 ldc 指令時,StringTable 是否已經記錄了一個對應內容的 String 的引用。
用如下代碼作分析展現:
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = "xxx";
}
複製代碼
查看其編譯後的 class 文件以下:
String s1 = "abc";
resolve 過程在字符串常量池中發現沒有」abc「的引用,便在堆中新建一個」abc「的對象,並將該對象的引用存入到字符串常量池中,而後把這個引用返回給 s1。
String s2 = "abc";
resolve 過程會發現 StringTable 中已經有了」abc「對象的引用,則直接返回該引用給 s2,並不會建立任何對象。
String s3 = "xxx";
同第一行代碼同樣,在堆中建立對象,並將該對象的引用存入到 StringTable,最後返回引用給 s3。
常量池與 intern 方法
public static void main(String[] args) {
String s1 = "ab";//#1
String s2 = new String(s1+"d");//#2
s2.intern();//#3
String s4 = "xxx";//#4
String s3 = "abd";//#5
System.out.println(s2 == s3);//true
}
複製代碼
查看其編譯後的 class 文件以下:
經過 class 文件信息可知,「ab」、「d」、「xxx」,「abd」進入到了 class 文件常量池,因爲類在 resolve 階段是 lazy 的,因此是不會建立實例對象,更不會駐留字符串常量池。
圖解以下:
注意此時沒有把「abd」對象的引用放入字符串常量池。
一、全局字符串常量池在每一個 VM 中只有一份,存放的是字符串常量的引用值。
二、class 常量池是在編譯的時候每一個 class 都有的,在編譯階段,存放各類字面量和符號引用。
三、運行時常量池是在類加載完成以後,將每一個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每一個 class 都有一個運行時常量池,類在解析以後,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。
四、class 文件常量池中的字符串字面量在類加載時進入到運行時常量池,在真正在 resolve 階段(即執行 ldc 指令時)時將該字符串的引用存入到字符串常量池中,另外運行時常量池相對於 class 文件常量池具有動態性,有些常量不必定在編譯期產生,也就是並不是預置入 class 文件常量池的內容才能進入到方法區運行時常量池,運行期間經過 intern 方法,將字符串常量存入到字符串常量池中和運行時常量池(關於優先進入到哪一個常量池,私覺得先進入到字符串常量池,具體實現還望大神指教)。