1. 程式人生 > >Java中幾種常量池的區分

Java中幾種常量池的區分

加載完成 表結構 結構 reference 嘗試 int 理解 方法區 spa

轉載自:https://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/

在java的內存分配中,經常聽到很多關於常量池的描述,我開始看的時候也是看的很模糊,網上五花八門的說法簡直太多了,最後查閱各種資料,終於算是差不多理清了,很多網上說法都有問題,筆者嘗試著來區分一下這幾個概念。

1.全局字符串池(string pool也有叫做string literal pool)

全局字符串池裏的內容是在類加載完成,經過驗證,準備階段之後在堆中生成字符串對象實例,然後將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放的。

)。
在HotSpot VM裏實現的string pool功能的是一個StringTable類,它是一個哈希表,裏面存的是駐留字符串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字符串實例本身),也就是說在堆中的某些字符串實例被這個StringTable引用之後就等同被賦予了”駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。

2.class文件常量池(class constant pool)

我們都知道,class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)


字面量就是我們所說的常量概念,如文本字符串、被聲明為final的常量值等。
符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。一般包括下面三類常量:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

常量池的每一項常量都是一個表,一共有如下表所示的11種各不相同的表結構數據,這每個表開始的第一位都是一個字節的標誌位(取值1-12),代表當前這個常量屬於哪種常量類型。

每種不同類型的常量類型具有不同的結構,具體的結構本文就先不敘述了,本文著重區分這三個常量池的概念(讀者若想深入了解每種常量類型的數據結構可以查看《深入理解java虛擬機》第六章的內容)。

3.運行時常量池(runtime constant pool)

當java文件被編譯成class文件之後,也就是會生成我上面所說的class常量池,那麽運行時常量池又是什麽時候產生的呢?

jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中後,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的並不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之後,也就是把符號引用替換為直接引用,解析的過程會去查詢全局字符串池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。

舉個實例來說明一下:

1 String str1 = "abc";
2 String str2 = new String("def");
3 String str3 = "abc";
4 String str4 = str2.intern();
5 String str5 = "def";
6 System.out.println(str1 == str3);//true
7 System.out.println(str2 == str4);//false
8 System.out.println(str4 == str5);//true

上面程序的首先經過編譯之後,在該類的class常量池中存放一些符號引用,然後類加載之後,將class常量池中存放的符號引用轉存到運行時常量池中,然後經過驗證,準備階段之後,在堆中生成駐留字符串的實例對象(也就是上例中str1所指向的”abc”實例對象),然後將這個對象的引用存到全局String Pool中,也就是StringTable中,最後在解析階段,要把運行時常量池中的符號引用替換成直接引用,那麽就直接查詢StringTable,保證StringTable裏的引用值與運行時常量池中的引用值一致,大概整個過程就是這樣了。

回到上面的那個程序,現在就很容易解釋整個程序的內存分配過程了,首先,在堆中會有一個”abc”實例,全局StringTable中存放著”abc”的一個引用值,然後在運行第二句的時候會生成兩個實例,一個是”def”的實例對象,並且StringTable中存儲一個”def”的引用值,還有一個是new出來的一個”def”的實例對象,與上面那個是不同的實例,當在解析str3的時候查找StringTable,裏面有”abc”的全局駐留字符串引用,所以str3的引用地址與之前的那個已存在的相同,str4是在運行的時候調用intern()函數,返回StringTable中”def”的引用值,如果沒有就將str2的引用值添加進去,在這裏,StringTable中已經有了”def”的引用值了,所以返回上面在new str2的時候添加到StringTable中的 “def”引用值,最後str5在解析的時候就也是指向存在於StringTable中的”def”的引用值,那麽這樣一分析之後,下面三個打印的值就容易理解了。

總結

  • 1.全局常量池在每個VM中只有一份,存放的是字符串常量的引用值。
  • 2.class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。
  • 3.運行時常量池是在類加載完成之後,將每個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每個class都有一個運行時常量池,類在解析之後,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。

Java中幾種常量池的區分