class文件之常量池分析
當(dāng)JVM運行Java程序的時候,它會加載對應(yīng)的class文件,并提取class文件中的信息存放在JVM開辟出來的方法區(qū) 內(nèi)存中。那么這個class文件里面到底有些什么內(nèi)容呢?
一、class文件內(nèi)容概述
class文件是由8bits的字節(jié)流組成,全部字節(jié)構(gòu)成了15個有意義的項目。這些項目之間沒有任何無意義的字節(jié),因此class文件非常緊湊。占據(jù)多字節(jié)空間的項目按照高位在前的順序存放。下面我們詳細(xì)討論這些項目:
★ magic(魔數(shù)) 每個class文件的前4個字節(jié)稱為魔數(shù),值為0xCAFEBABE。作用在于輕松的辨別class文件與非class文件。
★ minor_version、major_version(次、主版本號) 各占2個字節(jié)。隨著Java技術(shù)的發(fā)展,class文件的格式會發(fā)生變化。版本號的作用在于使得虛擬機(jī)能夠認(rèn)識當(dāng)前加載class的文件格式。從而準(zhǔn)確的提取class文件信息。
★ constant_pool_count 、constance_pool(常量池) 從這里開始的字節(jié)組成了常量池 。 存儲了諸如符號常量、final常量值、基本數(shù)據(jù)類型的字面值等內(nèi)容。JVM會將每一個常量構(gòu)成一個常量表,每個常量表都有自己的入口地址。而實際上在JVM會將這些常量表存儲在方法區(qū)中一塊連續(xù)的內(nèi)存空間中,因此class文件會根據(jù)常量表在常量池中的位置對其進(jìn)行索引。比如常量池中的第一個常量表的索引值就是1,第二個就是2。有的時候常量表A需要常量表B的內(nèi)容,則在常量表A中會存儲常量表B的索引值x。而constant_pool_count就記錄了有多少個常量表,或則所有多少個索引值。實際上,常量池中沒有索引值為0的常量表,但這缺失的索引值也被記錄在constant_pool_count中,因此 constant_pool_count等于常量表的數(shù)量加1。關(guān)于常量池的具體內(nèi)容,我們會在下面詳細(xì)講述,并用一個例子來顯示整個class文件的內(nèi)容。
★ access_flags(訪問標(biāo)志) 占用2個字節(jié)。用來表明該class文件中定義的是類還是接口,訪問修飾符是public還是缺省。類或接口是否是抽象的。類是否是final的。
★ this_class 占用2個字節(jié)。 它是一個對常量池的索引。指向的是常量池中存儲類名符號引用的CONSTANT_Class_info常量表(見下面常量池具體結(jié)構(gòu))。比如this_class=0x0001。則表示指向常量池中的第一個常量表。通常這個表是指向當(dāng)前class文件所定義的類名。
★ super_class 占用2個字節(jié) 與this_class類似,指向存放當(dāng)前class文件所定義類的超類名字的索引的 CONSTANT_Class_info常量表。
★ inteface_count、interfaces interface_count是class文件所定義的類直接實現(xiàn)的接口或父類實現(xiàn)的接口的數(shù)量。占2個字節(jié)。intefaces包含了對每個接口的 CONSTANT_Class_info常量表的索引。
★fields_count、fields fields_count表明了類中字段的數(shù)量 。fields是不同長度的field_info表的序列。這些field_info表中并不包含超類或父接口繼承而來的字段。field_info表展示了一個字段的信息,包括字段的名字,描述符和修飾符。如果該字段是final的,那么還會展示其常量值。注意,這些信息有些存放在field_info里面,有些則存放在field_info所指向的常量池中。下面我們闡述一下這個field_info表的格式:
access_flags(2byte 訪問修飾符)
name_index(2byte 存儲字段名的常量表在常量池中的索引)
description_index(2byte 存儲字段的所屬類型的常量表在常量池中的索引)
attribute_count(2byte 屬性表的數(shù)量)
attribute (屬性)
其中attribute是由多個attribute_info組成。而JVM規(guī)范定義了字段的三種屬性:ConstanceValue、Deprecated和Synthetic。
★method_count、methods 與字段類似,method_count表明類中方法的數(shù)量和每個方法的常量表的索引。methods表明了不同長度的method_info表的序列。該表格式如下:
access_flags(2byte 訪問修飾符)
name_index(2byte 存儲方法名的常量表在常量池中的索引)
description_index(2byte 存儲方法的返回類型和參數(shù)類型的常量表在常量池中的索引)
attribute_count(2byte 屬性表的數(shù)量)
attribute (屬性)
其中方法的屬性JVM規(guī)定了四種:Code,Deprecated,Exceptions,Synthetic。
二、常量池的具體結(jié)構(gòu)
在Java程序中,有很多的東西是永恒的,不會在運行過程中變化。比如一個類的名字,一個類字段的名字/所屬類型,一個類方法的名字/返回類型/參數(shù)名與所屬類型,一個常量,還有在程序中出現(xiàn)的大量的字面值。比如下面小段源碼紅色顯示的東西。
public class ClassTest {
private String itemS ="我們 ";
private final int itemI =100 ;
public void setItemS (String para ){...}
}
而這些在JVM解釋執(zhí)行程序的時候是非常重要的。那么編譯器將源程序編譯成class文件后,會用一部分字節(jié)分類存儲這些永恒不變的紅色東西。而這些字節(jié)我們就成為常量池。事實上,只有JVM加載class后,在方法區(qū)中為它們開辟了空間才更像一個“池”。
正如上面所示,一個程序中有很多永恒的紅色東西。每一個都是常量池中的一個常量表(常量項)。而這些常量表之間又有不同,class文件共有11種常量表,如下所示:
常量表類型 | 標(biāo)志值(占1 byte) | 描述 |
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 | 對一個類或接口的符號引用 |
CONSTANT_String | 8 | String類型字面值的引用 |
CONSTANT_Fieldref | 9 | 對一個字段的符號引用 |
CONSTANT_Methodref | 10 | 對一個類中方法的符號引用 |
CONSTANT_InterfaceMethodref | 11 | 對一個接口中方法的符號引用 |
CONSTANT_NameAndType | 12 | 對一個字段或方法的部分符號引用 |
(1) CONSTANT_Utf8 用UTF-8編碼方式來表示程序中所有的重要常量字符串。這些字符串包括: ①類或接口的全限定名, ②超類的全限定名,③父接口的全限定名, ④類字段名和所屬類型名,⑤類方法名和返回類型名、以及參數(shù)名和所屬類型名。⑥字符串字面值
表格式: tag(標(biāo)志1:占1byte) length(字符串所占字節(jié)的長度,占2byte) bytes(字符串字節(jié)序列)
(2) CONSTANT_Integer、 CONSTANT_Float、 CONSTANT_Long、 CONSTANT_Double 所有基本數(shù)據(jù)類型的字面值。比如在程序中出現(xiàn)的1用CONSTANT_Integer表示。3.1415926F用 CONSTANT_Float表示。
表格式: tag bytes(基本數(shù)據(jù)類型所需使用的字節(jié)序列)
(3) CONSTANT_Class 使用符號引用來表示類或接口。我們知道所有類名都以 CONSTANT_Utf8表的形式存儲。但是我們并不知道CONSTANT_Utf8表中哪些字符串是類名,那些是方法名。因此我們必須用一個指向類名字符串的符號引用常量來表明。
表格式: tag name_index(給出表示類或接口名的CONSTANT_Utf8表的索引)
(4) CONSTANT_String 同 CONSTANT_Class,指向包含字符串字面值的 CONSTANT_Utf8表。
表格式: tag string_index(給出表示字符串字面值的CONSTANT_Utf8表的索引)
(5) CONSTANT_Fieldref 、 CONSTANT_Methodref、 CONSTANT_InterfaceMethodref 指向包含該字段或方法所屬類名的CONSTANT_Utf8表,以及指向包含該字段或方法的名字和描述符的 CONSTANT_NameAndType 表
表格式: tag class _index(給出包含所屬類名的CONSTANT_Utf8表的索引) name_and_type_index(包含字段名或方法名以及描述符的 CONSTANT_NameAndType表 的索引)
(6) CONSTANT_NameAndType 指向包含字段名或方法名以及描述符的 CONSTANT_Utf8表。
表格式: tag name_index(給出表示字段名或方法名的CONSTANT_Utf8表的索引) type_index(給出表示描述符的CONSTANT_Utf8表的索引)
下面是我將一個源程序編譯成class文件后,對文件中的每一個字節(jié)的分析,可以更好的理解class文件的內(nèi)容以及常量池的組成。
三、TestClass.class 文件實例分析
- //源代碼
- package hr.test;
- //ClassTest類
- public class ClassTest {
- private int itemI=0; //itemI類字段
- private static String itemS="我們"; //itemS類字段
- private final float PI=3.1415926F; //PI類字段
- //構(gòu)造器方法
- public ClassTest(){
- }
- //getItemI方法
- public int getItemI(){
- return this.itemI;
- }
- //getItemS方法
- public static String getItemS(){
- return itemS;
- }
- //main主方法
- public static void main(String[] args) {
- ClassTest ct=new ClassTest();
- }
- }
TestClass.class 字節(jié)碼分析(字節(jié)順序從上到下,從左到右。每個字節(jié)用一個0-255的十進(jìn)制整數(shù)表示)
202 254 186 190 -- 魔數(shù)
0 0 -- 次版本號
0 50 -- 主版本號
0 43 -- 常量池中常量表的數(shù)量有42個,下面紅色括號中的數(shù)據(jù)表明該常量表所在常量池中的索引,從索引1開始
(1) 7 0 2 -- 對類ClassTest的符號引用(7為標(biāo)志 02指向了常量池的索引2的位置)
(2) 1 0 17 104 114 47 116 101 115 116 47 67 108 97 115 115 84 101 115 116 -- 類全限定名hr\test\ClassTest
(3) 7 0 4 -- 對類Object的符號引用
(4) 1 0 16 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 116 -- 超類全限定名 java/lang/Object
(5) 1 0 5 105 116 101 109 73 -- 第1個類字段名 itemI
(6) 1 0 1 73 -- I 第1個類字段類型為整型
(7) 1 0 5 105 116 101 109 83 -- 第2個類字段名 itemS
(8) 1 0 18 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 -- 第2個類字段類型的全限定名 Ljava/lang/String
(9) 1 0 2 80 73 -- 第3個類字段名PI
(10) 1 0 1 70 -- 第3個類字段類型為float
(11) 1 0 13 67 111 110 115 116 97 110 116 86 97 108 117 101 --- 第3個類字段為常量ConstantValue
(12) 4 64 73 15 218 -- 第3個類字段float字面值,占4bytes(3.1415926)
(13) 1 0 8 60 99 108 105 110 105 116 62 -- <clinit> 初始化方法名
(14) 1 0 3 40 41 86 -- ()V 方法的返回類型為void
(15) 1 0 4 67 111 100 101 -- Code
(16) 8 0 17 -- String字符串字面值(0 17表示索引1 7)
(17) 1 0 6 230 136 145 228 187 172 -- "我們"
(18) 9 0 1 0 19 -- 指向 第2個 字段的引用(0 1指向索引1,0 19指向索引19)
(19) 12 0 7 0 8 --指向 第2個 字段的名字和描述符的索引,
(20) 1 0 15 76 105 110 101 78 117 109 98 101 114 84 97 98 108 101 -- LineNumberTable
(21) 0 18 76 111 99 97 108 86 97 114 105 97 98 108 101 84 97 98 108 101 -- LocalVariableTable
(22) 1 0 6 60 105 110 105 116 62 -- <init> 表示初始化方法名
(23) 10 0 3 0 24 -- 指向父類Object的構(gòu)造器方法,0 3表示父類名常量表的索引,0 24表示存放該方法名稱和描述符的引用的常量表的索引
(24) 12 0 22 0 14 -- 指向方法名和描述符的常量表的索引。0 22是方法名的常量表索引,0 14是描述符的常量表索引
(25) 9 0 1 0 26 -- 指向第1個字段的引用, 0 1表示字段所屬類型的索引,0 26表示字段名和描述符的索引
(26) 12 0 5 0 6 -- 指向第1個字段的名字和描述符的索引
(27) 9 0 1 0 28 -- 指向第3個字段的引用, 0 1表示字段所屬類型的索引,0 28表示字段名和描述符的索引
(28) 12 0 9 0 10 -- 指向第3個字段的名字和描述符的索引
(29) 1 0 4 116 104 105 115 -- 隱含參數(shù)符號this
(30) 1 0 11 76 67 108 97 115 115 84 101 115 116 59 -- LClassTest;
(31) 1 0 8 103 101 116 73 116 101 109 73 - - 方法名 getItemI
(32) 1 0 3 40 41 73 -- ()I 方法描述符:返回類型int
(33) 1 0 8 103 101 116 73 116 101 109 83 -- 方法名 getItemS
(34) 1 0 20 40 41 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 --- 方法描述符()Ljava/lang/String;
(35) 1 0 4 109 97 105 110 -- 主方法名main
(36) 1 0 22 40 91 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 41 86 --- ()Ljava/lang/String;)V 主方法中的參數(shù)的字符串?dāng)?shù)組類型名
(37) 10 0 1 0 24 指向當(dāng)前 ClassTest 類的構(gòu)造器方法,0 1表示存放當(dāng)前類名的常量表的索引。0 24是存放方法名和描述符的符號引用的常量表索引。
(38) 1 0 4 97 114 103 115 -- 參數(shù)args
(39) 1 0 19 91 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 -- 字符串?dāng)?shù)組 [Ljava/lang/String;
(40) 1 0 2 99 116 --- 對象符號ct
(41) 1 0 10 83 111 117 114 99 101 70 105 108 101 -- SourceFile
(42) 1 0 14 67 108 97 115 115 84 101 115 116 46 106 97 118 97 -- ClassTest.java
0 33 ---- access_flag 訪問標(biāo)志 public
0 1 ---- this_class 指向當(dāng)前類的符號引用在常量池中的索引
0 3 ---- super_class
0 0 ---- inteface_count接口的數(shù)量
0 3 --- field_count字段的數(shù)量
// 字段 itemI
0 2 ---- private 修飾符
0 5 ---- 字段名在常量池中的索引,字段itemI
0 6 ---- 字段的描述符(所屬類型)在常量池中的索引
0 0 --- 字段的屬性信息表(attribute_info)的數(shù)量
// 字段 itemS
0 10 ---- private static 修飾符
0 7 ---字段名在常量池中的索引,字段itemS
0 8 ---字段的描述符(所屬類型)在常量池中的索引
0 0 --- 字段的屬性信息表(attribute_info)的數(shù)量
// 字段 PI
0 18 -- private final 修飾符
0 9 ---字段名在常量池中的索引,//字段PI
0 10 ---字段的描述符(所屬類型)在常量池中的索引
0 1 --- 字段的屬性信息表(attribute_info)的數(shù)量
0 11 --- 屬性名在常量池中的索引。即ConstantValue
0 0 0 2 --- 屬性所占的字節(jié)長度
0 12 --- 屬性值在常量池中的索引。即常量字面值
0 5 -- Method_count方法的數(shù)量
//類的靜態(tài)數(shù)據(jù)初始化方法<clinit>
0 8 ---- static 修飾符(所有的初始化方法都是static的)
0 13 --- 在常量池中的索引。初始化方法名<clinit>,該方法直接由JVM在特定的時候調(diào)用,并非由字節(jié)碼生成。
0 14 --- 在常量池中的索引。返回類型為void。
0 1 --- 屬性數(shù)量
0 15 -- 屬性名 在常量池中的索引。即code
0 0 0 42 --- 屬性所占的字節(jié)長度2
0 1 0 0 0 0 0 6 18 16 179 0 18 177 0 0 0 2 0 20 0 0 0 10 0 2 0 0 0 5 0 5 0 2 0 21 0 0 0 2 0 0 ---該方法的字節(jié)碼指令序列和其他信息
//類的普通實例數(shù)據(jù)的初始化方法,針對類構(gòu)造器生成的<init>方法。
0 1 --- public 修飾符
0 22 --- 構(gòu)初始化方法名<init>
0 14 --- 構(gòu)造器的返回類型為void
0 1 --- 屬性數(shù)量
0 15 --- 屬性名在常量池中的索引。即Code
0 0 0 70 -- 屬性所占的字節(jié)長度70
0 2 0 1 0 0 0 16 42 183 0 23 42 3 181 0 25 42 18 12 181 0 27 177 0 0 0 2 0 200 0 0 18 0 4 0 0 0 8 0 4 0 4 0 9 0 6 0 15 0 9 0 21 0 0 0 12 0 10 0 0 16 0 29 0 30 0 0 ---該方法的字節(jié)碼指令序列和其他信息
//getItemI方法
0 1 --- public 修飾符
0 31 --- 在常量池中的索引。方法名getItemI
0 32 --- 在常量池中的索引。方法返回類型為int
0 1 -- 屬性數(shù)量
0 15 --- 屬性名在常量池中的索引。即Code
0 0 0 47 --- 屬性所占的字節(jié)長度70
0 1 0 1 0 0 0 5 42 180 0 25 172 0 0 0 2 0 20 0 0 0 6 0 1 0 0 0 12 0 21 0 0 0 12 0 1 0 0 0 5 0 29 0 30 0 0 --- 該方法的字節(jié)碼指令序列和其他信息
//getItemS方法
0 9 --- public static 修飾符
0 33 --- 在常量池中的索引。方法名getItemS
0 34 - -- 在常量池中的索引。方法返回類型為String
0 1 --- 屬性數(shù)量
0 15 -- 屬性名在常量池中的索引。即Code
0 0 0 36 --- 屬性所占的字節(jié)長度36
0 1 0 0 0 0 0 4 178 0 18 176 0 0 0 2 0 20 0 0 0 6 0 1 0 0 0 16 0 21 0 0 0 2 0 0 --該方法的字節(jié)碼指令序列和其他信息
//main方法
0 9 --- public static 修飾符
0 35 --- 在常量池中的索引。主方法名main
0 36 -- 在常量池中的索引。方法返回類型為String[]
0 1 --- 屬性數(shù)量
0 15 --- 屬性名在常量池中的索引。即Code
0 0 0 65 --- 屬性所占的字節(jié)長度36
0 2 0 2 0 0 0 9 187 0 1 89 183 0 37 76 177 0 0 0 2 0 20 0 0 0 10 0 2 0 0 0 20 0 8 0 21 0 21 0 0 0 22 0 2 0 0 0 9 0 38 0 39 0 0 0 8 0 1 0 40 0 30 0 1 0 1 0 41 0 0 0 2 0 42
我們分析上面的字節(jié)碼例子,不難看出:
藍(lán)色背景的常量池字節(jié)碼區(qū)域:
(1) 所有的字面值都是存放在常量池中的。 特別注意的是“我們”這個字符串常量也是在常量池中的。如果一個程序出現(xiàn)多個“我們”,那么常量池中也只會有一個。另外,也正是因為“我們”存放在常量池中,使得一些字符串的==比較變的需要琢磨了。
(2)ClassTest并沒有任何顯示的父類。但在常量池中,我們發(fā)現(xiàn)有Object的符號常量存在。 這也證實了在Java中,任何類都直接或間接繼承了Object的,而Object并不需要在代碼中顯示繼承,JVM會幫我們做到這一點。
(3)常量池中有一個隱含參數(shù)this的符號常量。即使程序中不存在this,JVM也會悄悄的設(shè)置一個這樣的對象。
綠色背景的類字段字節(jié)碼區(qū)域:
(1)字段PI是浮點型常量,在編譯期的字節(jié)碼中就已經(jīng)指定好了PI的字面值存儲在常量池中的某個索引內(nèi) 。這一點也證實了Java中的常量在編譯期就已經(jīng)得到了值,在運行過程中是無法改變的。
橙色背景的類方法字節(jié)碼區(qū)域:
(1)主方法main是作為ClassTest的類方法存在的,在字節(jié)碼中main和其他的類方法并沒有什么區(qū)別。 實際上,我們也確實可以通過ClassTest.main(..)來調(diào)用ClassTest中的main方法。
(2)在class文件常量池字節(jié)碼中有兩個比較特別的方法名符號:<clinit>和<init>。其中<clinit>方法是編譯器自己生成的,編譯器會把類靜態(tài)變量的直接初始化語句和靜態(tài)初始化語句塊的代碼都放到了class文件的<clinit>方法中。而對所有非靜態(tài)非常量數(shù)據(jù)域的初始化工作要靠<init>方法來完成。針對每一個類的構(gòu)造方法,編譯器都會產(chǎn)生一個<init>方法。即使是缺省構(gòu)造器也不例外。