Java字節(jié)碼結(jié)構(gòu)剖析一:常量池

這篇博客開始,我打算帶大家去解讀一下JVM平臺下的字節(jié)碼文件(熟悉而又陌生的感覺)。眾所周知,Class文件包含了我們定義的類或接口的信息。然后字節(jié)碼又會被JVM加載到內(nèi)存中,供JVM使用。那么,類信息到了字節(jié)碼文件里,它們?nèi)绾伪硎镜模约霸谧止?jié)碼里是怎么分布的呢?帶著這些問題,讓我們?nèi)ド钊肓私庾止?jié)碼文件吧。

Class文件的結(jié)構(gòu)

Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內(nèi)容幾乎全部是程序運行的必要數(shù)據(jù),沒有空隙存在。當(dāng)遇到需要占用8位字節(jié)以上空間地數(shù)據(jù)項時,則會按照高位在前的方式分割成若干個8位字節(jié)進(jìn)行存儲。

每一個 Class 文件對應(yīng)于一個如下所示的 ClassFile 結(jié)構(gòu)體。

ClassFile {

????u4 magic;

????u2 minor_version;

????u2 major_version;

????u2 constant_pool_count;

????cp_info constant_pool[constant_pool_count-1];

????u2 access_flags;

????u2 this_class;

????u2 super_class;

????u2 interfaces_count;

????u2 interfaces[interfaces_count];

????u2 fields_count;

????field_info fields[fields_count];

????u2 methods_count;

????method_info methods[methods_count];

????u2 attributes_count;

????attribute_info attributes[attributes_count];

}

這種數(shù)據(jù)結(jié)構(gòu),類似C語言結(jié)構(gòu)體。這個結(jié)構(gòu)體中只有兩種數(shù)據(jù)類型:無符號數(shù)和表,后面的解析都要以這兩種數(shù)據(jù)類型為基礎(chǔ),所以這里要先介紹這兩個概念。

無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1,u2,u4,u8來分別代表1個字節(jié),2個字節(jié),4個字節(jié)和8個字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值。

是由多個無符號數(shù)或者其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型,所有表都習(xí)慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個Class文件本質(zhì)就是一張表。

下面是我的案例代碼,本章將以此代碼生成的字節(jié)碼文件作為例子來分析。

publicclassMyTest2 {

????String str = "Welcome";

????privateintx = 5;

????publicstaticInteger in = 10;


????publicstaticvoidmain(String[] args) {

????????MyTest2 myTest2 = newMyTest2();

????????myTest2.setX(8);

????????in = 20;

????}


????publicvoidsetX(intx) {

????????this.x = x;

????}

}

對應(yīng)生成的字節(jié)碼文件格式如下:(數(shù)據(jù)內(nèi)容較多,只是截了部分)

上面的數(shù)字是以16進(jìn)制表示的。我們可以按照之前的結(jié)構(gòu)一項項去解讀它。

Class文件解析

magic

魔數(shù),u4類型的數(shù)據(jù),占4個字節(jié)。魔數(shù)的唯一作用是確定這個文件是否為一個能被虛擬機所接受的 Class 文件。魔數(shù)值固定為?0xCAFEBABE(咖啡寶貝),不會改變。

minor_version、major_version

緊接著魔數(shù)之后的4個字節(jié)為Java版本信息:第5和第6個字節(jié)是次版本號(minor_version),第7和第8個字節(jié)是主版本號(major_version)。

就看當(dāng)前這個字節(jié)碼,次版本號是0×0000=0,主版本號是0×0034=52。我本地機器用的是JDK1.8,所以可生成的Class文件主版本號最大值為52.0。

下面給出了Java各個主版本號,以供參考。

constant_pool_count

常量池計數(shù)器,u2類型的數(shù)據(jù)。它是常量池的入口,表示緊跟著它后面的常量池的元素個數(shù)。算一下,0x002F=47,即常量池里的元素有47個。這里我用jdk的內(nèi)置工具javap,反編譯一下,可以輸出常量池的信息以及元素個數(shù)。執(zhí)行命令:javap -verbose。輸出結(jié)果如下:

Constant pool:

???#1 = Methodref????????? #10.#34??????? // java/lang/Object."<init>":()V

???#2 = String???????????? #35??????????? // Welcome

???#3 = Fieldref?????????? #5.#36???????? // com/shengsiyuan/jvm/bytecode/MyTest2.str:Ljava/lang/String;

???#4 = Fieldref?????????? #5.#37???????? // com/shengsiyuan/jvm/bytecode/MyTest2.x:I

???#5 = Class????????????? #38??????????? // com/shengsiyuan/jvm/bytecode/MyTest2

???#6 = Methodref????????? #5.#34???????? // com/shengsiyuan/jvm/bytecode/MyTest2."<init>":()V

???#7 = Methodref????????? #5.#39???????? // com/shengsiyuan/jvm/bytecode/MyTest2.setX:(I)V

???#8 = Methodref????????? #40.#41??????? // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

???#9 = Fieldref?????????? #5.#42???????? // com/shengsiyuan/jvm/bytecode/MyTest2.in:Ljava/lang/Integer;

??#10 = Class????????????? #43??????????? // java/lang/Object

??#11 = Utf8?????????????? str

??#12 = Utf8?????????????? Ljava/lang/String;

??#13 = Utf8?????????????? x

??#14 = Utf8?????????????? I

??#15 = Utf8?????????????? in

??#16 = Utf8?????????????? Ljava/lang/Integer;

??#17 = Utf8?????????????? <init>

??#18 = Utf8?????????????? ()V

??#19 = Utf8?????????????? Code

??#20 = Utf8?????????????? LineNumberTable

??#21 = Utf8?????????????? LocalVariableTable

??#22 = Utf8?????????????? this

??#23 = Utf8?????????????? Lcom/shengsiyuan/jvm/bytecode/MyTest2;

??#24 = Utf8?????????????? main

??#25 = Utf8?????????????? ([Ljava/lang/String;)V

??#26 = Utf8?????????????? args

??#27 = Utf8?????????????? [Ljava/lang/String;

??#28 = Utf8?????????????? myTest2

??#29 = Utf8?????????????? setX

??#30 = Utf8?????????????? (I)V

??#31 = Utf8?????????????? <clinit>

??#32 = Utf8?????????????? SourceFile

??#33 = Utf8?????????????? MyTest2.java

??#34 = NameAndType??????? #17:#18??????? // "<init>":()V

??#35 = Utf8?????????????? Welcome

??#36 = NameAndType??????? #11:#12??????? // str:Ljava/lang/String;

??#37 = NameAndType??????? #13:#14??????? // x:I

??#38 = Utf8?????????????? com/shengsiyuan/jvm/bytecode/MyTest2

??#39 = NameAndType??????? #29:#30??????? // setX:(I)V

??#40 = Class????????????? #44??????????? // java/lang/Integer

??#41 = NameAndType??????? #45:#46??????? // valueOf:(I)Ljava/lang/Integer;

??#42 = NameAndType??????? #15:#16??????? // in:Ljava/lang/Integer;

??#43 = Utf8?????????????? java/lang/Object

??#44 = Utf8?????????????? java/lang/Integer

??#45 = Utf8?????????????? valueOf

??#46 = Utf8?????????????? (I)Ljava/lang/Integer;

可是,我們得到的常量池里的元素個數(shù)是46。我們看常量池第一個元素,它的索引是從1開始的。所以索引值范圍是1~46。設(shè)計者將第0項常量空出來是有特殊考慮的,這樣做的目的在于滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)“不引用任何一個常量池項目”的含義,這種情況就可以把索引值置為0來表示。根本原因在于,索引為0也是一個常量(保留常量),只不過它不位于常量表中。這個常量就對應(yīng)Null值,所以常量池的索引從1而非0開始。

常量池結(jié)構(gòu)剖析

緊接其后的就是常量池了。一個Java類中定義的很多信息都是由常量池維護和描述的??梢詫⒊A砍乜醋魇荂lass文件的資源庫。比如:Java類中定義的方法與變量信息,都是存儲在常量池中。常量池中主要存儲兩類常量:字面常量和符號引用。字面量,如文本字符串,Java中聲明為常量值,而符號引用如類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。

注:常量池中存儲的不一定是不變的量!如,private int x = 5,x是變量,但“x”這個變量名字依然存在常量池中。

我們也可以把常量池當(dāng)做一個數(shù)組(常量池中的每一項常量都是一個表),與一般數(shù)組不同的是,常量池數(shù)組中不同的元素類型,結(jié)構(gòu)都是不同的,長度當(dāng)然也不相同;但是每一個元素的第一個數(shù)據(jù)都是u1類型,該字節(jié)是個標(biāo)志位,占一個字節(jié)。JVM在解析長量池時,會根據(jù)這個u1類型來獲取元素的具體類型。目前,常量池中出現(xiàn)的常量類型有14種,如下表:

有了這張表就可以繼續(xù)剖析常量池的內(nèi)容了,常量池第一個字節(jié)就是一個標(biāo)志位,0x000A=10,說明第一個常量類型是CONSTANT_Methodref_info。這是一個表類型,它對應(yīng)的結(jié)構(gòu)是:

CONSTANT_Methodref_info {

????u1 tag;

????u2 class_index;

????u2 name_and_type_index;

}

可知,該類型常量占1+2+2=5個字節(jié)。所以我們從常量池前5個字節(jié)就是第一個常量元素了。緊接后面就是第二個常量,同樣的,開始是一個標(biāo)志位,即0x008=8??芍诙€常量是CONSTANT_String_info類型。CONSTANT_String_info 用于表示java.lang.String類型的常量對象,格式如下:

CONSTANT_String_info {

????u1 tag;

????u2 string_index;

}

所以常量池的第二個元素占3個字節(jié)。按照這個套路,我們就可以找出每一個常量了。一直數(shù)到第46個常量,常量池就結(jié)束了。此處是常量池中的14種常量項的結(jié)構(gòu)總表。感興趣的可以對照這個表,去把剩下的常量對照出來。

常量項分析

第一個常量是CONSTANT_Methodref_info類型的,它描述了類中方法的符號引用。class_index 項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Class_info結(jié)構(gòu),表示一個類或接口。

class_index表示的索引值是0x000A=10。根據(jù)之前javap -verbose?輸出的常量池信息,我們可以知道常量池的#10項是CONSTANT_Class_info類型的常量。該類型常量用于表示類或接口,格式如下:

CONSTANT_Class_info {

????u1 tag;

????u2 name_index;

}

name_index 項的值,必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結(jié)構(gòu),代表一個有效的類或接口二進(jìn)制名稱的內(nèi)部形式。

name_index 表示的索引值是43(這里我直接從上面的量池信息讀出,如果從字節(jié)碼里看,此處的值為0x002B=43)。所以接著找常量池第43項的常量類型,是CONSTANT_utf8_info類型,用于表示字符串常量的值,結(jié)構(gòu)如下:

CONSTANT_Utf8_info {

????u1 tag;

????u2 length;

????u1 bytes[length];

}

其中,length 項的值指明了 bytes[]數(shù)組的長度,bytes[]是表示字符串值的byte數(shù)組。在這里,我把字節(jié)碼常量池中#43處常量的16進(jìn)制值單獨拿出來來看。下圖有背景色的部分就是完整的CONSTANT_Utf8_info類型常量表示。

第一個字節(jié)是標(biāo)志位,0×0001=1。說明此常量類型是CONSTANT_Utf8_info。后面2個字節(jié)是0×0010=16,表示后面bytes[]長度為16。所以往后數(shù)16個字節(jié)就是整個它表示的字符串常量。

bytes[]第一個字節(jié)值,0x006A。根據(jù)?ASCII碼對照表,代表的字符串是”j”。依次的,第二個字節(jié)0×0061,代表“a”,等等。把16個字節(jié)看完你就得到了字符串常量表示“java/lang/Object”。好了這表示一個類的全限定名。饒了一大圈,終于找到最終要表示的常量信息了。

到此,我們把第一個常量的結(jié)構(gòu)中的class_index就解析完了,還剩一個name_and_type_index。它表示了常量池在該索引處的項必須是 CONSTANT_NameAndType_info結(jié)構(gòu),它表示當(dāng)前字段或方法的名字和描述符。后面大家可以根據(jù)常量池中的14種常量項的結(jié)構(gòu)總表,并結(jié)合javap得到的常量池信息,自己去分析每個常量在常量池里是怎么個回事。

總結(jié)

這篇文章介紹了,字節(jié)碼文件的結(jié)構(gòu)組成,并分析了魔數(shù)、次主版本號和常量池。尤其帶大家深入分析了常量池的組成結(jié)構(gòu),并拿例子中的常量池第一個常量作為案例,完整解析它在常量池中的各項引用。套路都是一樣的,常量池后面的常量,大家可以自己去分析了。你會發(fā)現(xiàn)類中有用的信息都存在了我們的常量池里,然后以索引的形式,給代碼使用。這也就是常量池作為class文件的資源倉庫的原因了。

歡迎工作一到五年的Java工程師朋友們加入Java架構(gòu)開發(fā): 854393687

群內(nèi)提供免費的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構(gòu)資料)合理利用自己每一分每一秒的時間來學(xué)習(xí)提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容