Java常量池解析與字符串intern簡介
在Java應(yīng)用程序運(yùn)行時(shí),Java虛擬機(jī)會保存一份內(nèi)部的運(yùn)行時(shí)常量池,它區(qū)別于class文件的常量池,是class文件常量池映射到虛擬機(jī)中的數(shù)據(jù)結(jié)構(gòu)。 關(guān)于class文件常量池的部分可以參考之前的博文實(shí)例探索Class文件。
1.CONSTANT_Class入口解析
數(shù)組類的符號解析較為特殊。若是基本類型數(shù)組,那么虛擬機(jī)將創(chuàng)建該基本類型的新數(shù)組類,并創(chuàng)建一個(gè)Class實(shí)例來代表該類型,數(shù)組類的定義類加載器為 啟動類加載器。若是引用類型的數(shù)組,那么在此之前還會進(jìn)行引用類型的解析,數(shù)組類的定義類加載器為引用類型的定義類加載器。
非數(shù)組類和接口的的解析將經(jīng)歷以下步驟:
(1).加載該類型和其所有的超類型
如果該類型在此之前已經(jīng)裝載到了虛擬機(jī)的當(dāng)前命名空間,那么直接使用已經(jīng)被裝載的類型即可,否則由引用的發(fā)起類的初始類加載器進(jìn)行加載。對目標(biāo)類型的超類的加載必然是在對當(dāng)前類型加載完的基礎(chǔ)上進(jìn)行的,因?yàn)橹挥屑虞d完當(dāng)前類型,才能從class文件的super_class域找到其直接超類的符號引用,再遞歸進(jìn)行解析和加載,直至java.lang.Object類。而在遞歸返回的過程中,會檢查interfaces域以查看實(shí)現(xiàn)或擴(kuò)展了哪些接口,并再次遞歸遍歷對接口的符號引用。
(2).檢查訪問權(quán)限
隨后是對目標(biāo)類型的連接和初始化,這樣才可以正常使用該類型。前面提到,對目標(biāo)類型的初始化需要其所有超類都必須進(jìn)行初始化(超接口不是必須的),并且,由于已經(jīng)對其超類進(jìn)行了加載,所以不必再依賴于自該類向Object類的解析順序,而是從Object類向該類進(jìn)行初始化。類型的連接和初始化步驟如下:
(3).類型校驗(yàn)
(4).類型準(zhǔn)備
(5).類型解析(可推遲)
注意該過程是對被引用類型及其超類的符號引用的解析,因?yàn)閷τ诒灰妙愋偷哪承┓栆貌粫⒖逃玫?,故該步驟之前是嚴(yán)格意義上屬于發(fā)起引用的類型的符號 解析的過程。只有在主動使用被引用類型的這些符號引用所指向的類型時(shí),才會對這些符號引用進(jìn)行解析,對其所指向的類型進(jìn)行裝載、連接和初始化。
(6).類型初始化
2.CONSTANT_Fieldref入口解析
由于一個(gè)類型不會含有其超類型所定義的字段,所以對目標(biāo)字段的搜索將會從字段所 指向的類型開始,從該類型開始搜索,再遞歸搜索其所實(shí)現(xiàn)或擴(kuò)展的接口,再遞歸搜索其超類,直至找到目標(biāo)字段,并會將運(yùn)行時(shí)常量池的該字段入口標(biāo)記為已解 析,并在該常量池的數(shù)據(jù)上改為對這個(gè)字段的直接引用。
3.CONSTANT_Methodref入口解析
與字段的搜索類似但有所不同,其搜索順序?qū)脑擃愋烷_始,再遞歸搜索其超類,在遞歸搜索其所實(shí)現(xiàn)或擴(kuò)展的接口。
4.CONSTANT_InterfaceMethodRef入口解析
對接口方法的搜索就是從被解析的接口開始,向其超接口遞歸搜索。
5.CONSTANT_String入口解析
Java虛擬機(jī)會將字符串處理為一個(gè)字符串對象加以維護(hù),而虛擬機(jī)所維護(hù)的就是一張 字符串池,它包含所有被”拘留”的字符串對象的引用。對CONSTANT_String常量池的解析首先就要查看字符串池中該字符串對象的引用是否存在, 如果存在則直接把常量池?cái)?shù)據(jù)解析為該字符串對象的引用,若不存在,那么就需要根據(jù)這個(gè)字符串序列創(chuàng)建一個(gè)字符串對象,并將其引用加入到字符串池中,并將常 量池?cái)?shù)據(jù)解析為該引用。
也可以使用String對象的intern對象來拘留一個(gè)字符串(注意并非字符串對象),若該字符串池中存在對該字 符串序列的對象的引用,那么直接返回該引用即可,否則,將會拘留該字符串,但注意拘留返回的字符串對象引用將不會指向原String對象,因?yàn)樵?String對象位于Java堆,而字符串池的對象是虛擬機(jī)所創(chuàng)建的,由虛擬機(jī)所維護(hù)。
- package com.ice.intern;
- public class InternTest {
- public static void main(String args[]){
- String a = new String("123");
- String b = a;
- String c = new String("123");;
- System.out.println("before intern:");
- System.out.println("a = b ? :" + (a == b));
- System.out.println("a = c ? :" + (a == c));
- a = a.intern();
- c = c.intern();
- System.out.println("after intern:");
- System.out.println("a = b ? :" + (a == b));
- System.out.println("a = c ? :" + (a == c));
- }
- }
結(jié)果如下:
(6).其他類型(數(shù)據(jù)基本類型)入口解析
直接使用常量池所包含的常量值即可
6.直接引用
常量池解析最終將符號引用替換成為直接引用。指向類型、類變量和類方法的直接引用可能為在方法區(qū)的指針。而指向?qū)嵗兞亢蛯?shí)例方法的直接引用是從對象映像的開始到該實(shí)例變量或方法表的偏移。
實(shí)例變量的組織方式為:從Object類開始到該實(shí)例的類型,將類中聲明的實(shí)例變量按在class文件中出現(xiàn)的順序依次放在對象映像中。
實(shí)例方法的組織方式較為類似:從Object類開始到該實(shí)例的類型,將類中聲明的實(shí)例方法指針按在class文件中出現(xiàn)的順序依次放在對象映像中。但對于重寫的方法將出現(xiàn)在超類對應(yīng)的位置(該方法***次出現(xiàn)的位置)。
但是訪問接口方法就不能簡單地通過方法表的偏移量來進(jìn)行訪問,而必須搜索對象的類的方法表來找到該方法。
比如Factory接口分別由A和B來實(shí)現(xiàn)其produce()方法,但由于A和B不能保證由同一個(gè)實(shí)現(xiàn)了Factory接口的超類派生,即有著同樣的produce()方法偏移,那么就無法通過方法表的偏移來訪問Factory的produce()方法。
7.裝載約束
對于一個(gè)類型指向另一個(gè)類型的符號引用,如果引用的類型和被引用類型并非由同一個(gè)初始加載器加載(可能通過用戶自定 義ClassLoader來實(shí)現(xiàn)),那么虛擬機(jī)就必須確保被引用類型在不同的命名空間中保持一致。這樣就通過自定義ClassLoader來加載不受信類 型后,就不會發(fā)生解析對被引用類型的符號引用時(shí),把受信的類型當(dāng)做已經(jīng)被解析過的不受信類型(因?yàn)閷Ψ椒ǖ姆栆弥挥袡?quán)限定名和描述符,并不會也無法得 知其初始類加載器),從而調(diào)用了不受信類型的方法訪問受信類型的受保護(hù)成員。