在class文件中,“常量池”是最復雜也最值得關注的內容。
Java是一種動態連接的語言,常量池的作用非常重要,常量池中除了包含代碼中所定義的各種基本類型(如int、long等等)和對象型(如String及數組)的常量值還,還包含一些以文本形式出現的符號引用,比如:
類和接口的全限定名;
字段的名稱和描述符;
方法和名稱和描述符。
在C語言中,如果一個程序要調用其它庫中的函數,在連接時,該函數在庫中的位置(即相對於庫文件開頭的偏移量)會被寫在程序中,在運行時,直接去這個地址調用函數;
而在Java語言中不是這樣,一切都是動態的。編譯時,如果發現對其它類方法的調用或者對其它類字段的引用的話,記錄進class文件中的,只能是一個文本形式的符號引用,在連接過程中,虛擬機根據這個文本信息去查找對應的方法或字段。
所以,與Java語言中的所謂“常量”不同,class文件中的“常量”內容很非富,這些常量集中在class中的一個區域存放,一個緊接著一個,這裡就稱為“常量池”。
常量池由多條“常量池項”組成,每一個常量池項又由兩部分組成,這裡分別稱為“常量池項頭”和“常量池項體”。
常量池項頭表明常量池項的類型,常量池項共分為11種類型,分別為:
常量池項類型
值
說明
CONSTANT_Utf8
1
UTF-8編碼的Unicode字符串
CONSTANT_Integer
3
int型常量
CONSTANT_Float
4
Float型常量
CONSTANT_Long
5
Long型常量
CONSTANT_Double
6
double型常量
CONSTANT_Class
7
對一個class的符號引用
CONSTANT_String
8
String型常量
CONSTANT_FIEldref
9
對一個字段的符號引用
CONSTANT_Methodref
10
對一個類方法的符號引用
CONSTANT_InterfaceMedthodref
11
對一個接口方法的符號引用
CONSTANT_NameAndType
12
對名稱和類型的符號引用
常量池項體中存放的就是對應的常量數據,比如各種數值型的常量或者字符串等等。
以下介紹kvm中的常量池是如何組織起來的。
數據結構:
在KVM的頭文件kvm/vmcommon/h/pool.h中,有以下對常量池項類型的定義:
#define CONSTANT_Utf8 1
#define CONSTANT_Integer 3
#define CONSTANT_Float 4
#define CONSTANT_Long 5
#define CONSTANT_Double 6
#define CONSTANT_Class 7
#define CONSTANT_String 8
#define CONSTANT_FIEldref 9
#define CONSTANT_Methodref 10
#define CONSTANT_InterfaceMethodref 11
#define CONSTANT_NameAndType 12
以及常量池項體結構的定義:
union constantPoolEntryStruct {
struct {
unsigned short classIndex;
unsigned short nameTypeIndex;
} method; /* Also used by FIElds */
CLASS clazz;
INTERNED_STRING_INSTANCE String;
cell *cache; /* Either clazz or String */
cell integer;
long length;
NameTypeKey nameTypeKey;
NameKey nameKey;
UString ustring;
};
class文件中,常量池項有很多種類,每一個常量池項的大小都不同,而對於常量池的使用又是如此之多,最好能夠使用數組來索引,這樣可以提高效率,所以KVM裡使用union來代表一個常池項,union的每一項是常量池項的一種可能的數據類型,這樣每一項都有了相同的大小,可以構造數組。
顯然,這個數組就將是常量池的核心內容,那麼這個數組放在哪裡呢?就在下面這個結構中:
struct constantPoolStruct {
union constantPoolEntryStruct entrIEs[1];
};
這就是常量池。這個常量池的設計很有意思:
1、這個結構體中只有一個指針,指向一個常量池項體數組,數組中元素的個數是常量池項數+1,數組中的第一項(即序號為0的那一項)不是實際的常量池項體,而是存放了常量池項的數目,即表明了數組中接下來的元素數。要取得數組的長度信息,只有一個辦法,就是讀數組的第一個元素,為不造成空指針錯誤,所以constantPoolStruct在定義的時候就要保證數組的第0個元素必須存在,所以上面的entrIEs在定義時就被指定為長度為1的數組。
單純從數據結構的設計角度來看,我認為constantPoolStruct的設計並不是很清晰,使用數組的第一個無素來表示數組的長度多少一點顯得混亂,明明可以在constantPoolStruct的結構裡增加一個變量來表明數組長度,這樣不是更清晰嗎?之所以這樣做,我想也是與class文件中常量池的設計慣例有關。在class文件中, constant_pool緊跟在constant_pool_count之後,而constant_pool_count = constant_pool中實際的項數+1,相當於constant_pool_count也把自己當成了常量池中的第一項。
由此可見,KVM的常量池設計與class文件如出一轍。
2、常量池項體以一個union來表示,而union不帶有自身類型的信息,如何知道一個常量池項的類型呢?
在一個class文件的常量池被載入後,生成了constantPoolStruct結構體的實例,在其中constantPoolEntryStruct數組的最後一項之後,一定會跟隨一個字節數組,這個數組中的每一個字節就是一個“常量池項頭”,長度與實際的常量池項數相同,即constant_pool_count-1,在這個字節中就指明了相應常量池項的類型。
程序實現:
構造常量池的代碼段主要在kvm/vmcommon/src/loader.c的loadConstantPool()函數中,函數原形如下:
static POINTERLIST
loadConstantPool(FILEPOINTER_HANDLE ClassFileH, INSTANCE_CLASS CurrentClass);
兩個參數分別為類文件的句柄以及當前被載入類的指針。
這個函數的總體流程如下:
1- 循環讀取文件中常量池中所有項,把,把各項內容存入臨時數組RowPool中;(L649~L740)
2- 計算常量池所占空間大小(以constantPoolEntryStruct枚舉體數計),並申請常量池空間;(L742~L757)
3- 循環讀取暫存在RowPool中的常量信息,為常量池賦值。
其中第2步值得一看,記算空間大小的那一行如下:
int tableSize = numberOfEntries + ((numberOfEntrIEs + (4 - 1)) >> 2);
一個constantPoolEntryStruct枚舉體的大小為4,前面講過,在constantPoolEntryStruct數組的後要跟有一個字節數組來存放常量池項的類型信息,即每一個constantPoolEntryStruct要對應1個字節的常量池項頭,所以當以constantPoolEntryStruct枚舉體數為單位給常量池項頭數組申請空間時,需要向4字節對齊,每多1~4個常量池項頭,就要多申請一個constantPoolEntryStruct。這一句就是這個意思。
loadConstantPool函數執行過程中,會把新生成的常量池指針賦給CurrentClass->constPool,這樣,這個類實例中就有完整的常量池了。