魔數(shù)與版本
常量池
類(lèi)、父類(lèi)和接口索引集合
字段表集合
方法表集合
屬性表集合
? ? ? ? Class文件(即字節(jié)碼文件)以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列, 中間沒(méi)有分隔符,沒(méi)有一個(gè)字節(jié)是多余的。文件格式是使用一種類(lèi)似于C語(yǔ)言中的結(jié)構(gòu)體來(lái)描述和存儲(chǔ)數(shù)據(jù)的,其中包括無(wú)符號(hào)數(shù)和表。無(wú)符號(hào)數(shù)是一種采用u1、u2、u4和u8來(lái)分別表示1個(gè)字節(jié)、2個(gè)字節(jié)、4個(gè)字節(jié)和8個(gè)字節(jié)的數(shù)據(jù)類(lèi)型,是方便用來(lái)表示字節(jié)的長(zhǎng)度的一種類(lèi)型。而表則是由多個(gè)無(wú)符號(hào)數(shù)或者其他表組合而成的復(fù)合類(lèi)型,習(xí)慣以“_info”結(jié)尾,如常量表CONSTANT_utf8_info。數(shù)據(jù)存儲(chǔ)的字節(jié)碼順序是采用大端法表示的,所謂大端法是最高有效字節(jié)在最前面,如0x1234,0x1地址在0x101,0x2地址在0x102,以此類(lèi)推。
? ? ? ?輔助工具:jdk8、notepad++(HEX_Editor插件)、javap、bytecode-viewer。notepad++需要安裝hex_editor插件,這是一個(gè)把二進(jìn)制轉(zhuǎn)化為十六進(jìn)制的一個(gè)插件,bytecode-viewer是一個(gè)字節(jié)碼解析工具,方便我們?nèi)ヲ?yàn)證解析結(jié)果正確與否。
? ? ? ?在一個(gè)字節(jié)碼文件中,可能有多個(gè)屬性表或者多個(gè)無(wú)符號(hào)數(shù),所以在字節(jié)碼里會(huì)先給出一個(gè)數(shù)量計(jì)數(shù)器,以表示接下來(lái)會(huì)有多少個(gè)類(lèi)型,這樣一系列連續(xù)的類(lèi)型就組成了某一類(lèi)的數(shù)據(jù)集合,而字節(jié)碼就是由下圖中的個(gè)種類(lèi)型集合組成。

? ? ? ?為了方便講述問(wèn)題,我們采用的代碼盡量簡(jiǎn)單,故采用以下代碼作為demo
?
package org.javersoft.clazz;
public class TestClass {
private int m;
? ? public int inc() {
return m +1;
? ? }
}
魔數(shù)與版本
? ? ? ?每個(gè)Class文件的頭4個(gè)字節(jié)稱(chēng)為魔數(shù),確定這個(gè)文件是否為一個(gè)能否被虛擬機(jī)接 受的Class文件,緊接著魔數(shù)的4個(gè)字節(jié)是Class文件的版本號(hào),第5和第6個(gè)字節(jié)是次版本號(hào),第7和 第8個(gè)字節(jié)是主版本號(hào)。如下圖,編譯后得到的class文件,有notepadd++打開(kāi),并用HEX_Editor插件轉(zhuǎn)換。


????????頭4個(gè)字節(jié)是cafebabe,而接下來(lái)4個(gè)字節(jié)就是版本號(hào),主版本號(hào)是0x0034,換做十進(jìn)制就是52,也就是jdk8的主版本。
常量池
? ? ? ?常量池中主要存放兩大類(lèi)常量:字面量和符號(hào)引用。字面量可理解為java語(yǔ)言層面的常量概念,符號(hào)引用包括3類(lèi)常量,類(lèi)和接口的全限定名、字段的名稱(chēng)和描述符、方法的名稱(chēng)和描述符。每個(gè)常量都有自己的標(biāo)志,如下圖
? ? ? ?緊接著主次版本號(hào)之后的是常量池的入口,常量池可以理解為Class文件之中的資源倉(cāng)庫(kù),先直觀看看,這個(gè)class文件都有哪些常量


? ? ? ?常量池中常量的數(shù)量不固定,需在入口處放置一項(xiàng)u2類(lèi)型的數(shù)據(jù),以表示常量池容量計(jì)數(shù)值,從1開(kāi)始,0空出來(lái)表示特殊情況,下面會(huì)有講到,以表示“不需要引用任何一個(gè)常量池項(xiàng)目”。

? ? ? ?主次版本之后的一項(xiàng)u2類(lèi)型的數(shù)據(jù)是0x0013,換做十進(jìn)制是19,就表示接下來(lái)會(huì)有19個(gè)常量,下面會(huì)接著一個(gè)個(gè)講,在講之前需要先了解一下常量池中常用的結(jié)構(gòu)類(lèi)型是怎樣的

? ? ? ?比如第一個(gè)CONSTANT_Utf8_info類(lèi)型,用來(lái)描述字符串,第一項(xiàng)是tag,tag的作用是標(biāo)識(shí)常量類(lèi)型的,而1就表示CONSTANT_Utf8_info類(lèi)型,占一個(gè)u1類(lèi)型長(zhǎng)度,即用1個(gè)字節(jié)來(lái)表示;第二項(xiàng)length用來(lái)表示后面字符串所占的長(zhǎng)度,其本身所能表示的長(zhǎng)度是u2,即2個(gè)字節(jié)所能表示的最大值;第三項(xiàng)就是長(zhǎng)度為length的utf-8編碼的字符串。后面的常量類(lèi)型也是這樣理解。且看第一個(gè)常量:



????????第一個(gè)tag值為10,對(duì)應(yīng)的是CONSTANT_Methodref_info常量,第一個(gè)u2類(lèi)型字節(jié)是0x0004,表示的是索引值為4的常量,其指向java.lang.Object類(lèi)描述符,第二個(gè)u2類(lèi)型字節(jié)是0x00f,表示的是索引值為15的常量,其指向init()實(shí)例構(gòu)造器的名稱(chēng)描述符,代碼并沒(méi)有init()方法,是編譯器自動(dòng)生成的,用javap命令查看得知。
? ? ? ?到此,第一個(gè)常量已經(jīng)解析出來(lái),接下來(lái)看第二個(gè)常量。


? ? ? ?第二個(gè)常量tag值為9,對(duì)應(yīng)CONSTANT_Fieldref_info,第一個(gè)u2是0x0003,指向索引值為3,索引3是一個(gè)CONSTANT_Class_info類(lèi)型,CONSTANT_Class_info的index又指向索引17,表示一個(gè)類(lèi)的全限定名,即org.javersoft.clazz.TestClass;?第二個(gè)u2是0x0010,指向索引值為16,索引是6是一個(gè)CONSTANT_NameAndType_info常量,其第一個(gè)index指向索引5,表示指向字段m的名稱(chēng)常量項(xiàng),第二個(gè)index索引指向6,表示指向字段描述符I,關(guān)于描述符后面再講。

? ? ? ?到此,第二個(gè)常量也解析出來(lái)了,接下來(lái)看第三個(gè)常量。


? ? ? ?第三個(gè)常量tag值為7,表示CONSTANT_Class_info類(lèi)型,后面0x0011指向索引17,索引17是一個(gè)CONSTANT_Utf8_info常量,表示TestClass類(lèi)的全限定名。

? ? ? ?第四個(gè)常量跟第三個(gè)常量一樣解析即可,接下來(lái)看第五個(gè)常量。


?? ? ?這個(gè)字符串的length值是0x0001,也就是長(zhǎng)1個(gè)字節(jié),即“0x6d”,按utf-8編碼,內(nèi)容為“m”。Class文件中的方法、字段等都需要引用CONSTANTS_Utf8_info型常量來(lái)描述名稱(chēng),從這里也看出,一個(gè)類(lèi)的字段名和方法名長(zhǎng)度不能超過(guò)0xFFFF個(gè)字節(jié)。到此5個(gè)常量已經(jīng)解析出來(lái)了,后面的13個(gè)常量也是這樣解析即可。我給每個(gè)常量劃分了一下,如下圖,就是說(shuō),18個(gè)常量,到0x74為止。

? ? ? ?訪問(wèn)標(biāo)志,用兩個(gè)字節(jié)代表訪問(wèn)標(biāo)志,用于識(shí)別一些類(lèi)或者接口層次的訪問(wèn)信息,包括:這個(gè)Class是類(lèi)還是接口;是否定義為public類(lèi)型;是否定義為abstract類(lèi)型;如果是類(lèi)的話,是否被聲明為final等


? ? ? ?0x0021,即除了ACC_PUBLIC和ACC_SUPER為真,其余為假,表示這個(gè)類(lèi)的修飾符是public
類(lèi)、父類(lèi)和接口索引集合
? ? ? ?類(lèi)索引:u2類(lèi)型,用于確定這個(gè)類(lèi)的全限定名
? ? ? ?父類(lèi)索引:u2類(lèi)型,用于確定這個(gè)類(lèi)的父類(lèi)的全限定名(除java.lang.Object外,所有java類(lèi) 都有父類(lèi)索引都不為0)
? ? ? ?接口索引:一組u2類(lèi)型的數(shù)據(jù)的集合,用于描述這個(gè)類(lèi)實(shí)現(xiàn)了哪些接口,入口第一項(xiàng)(u2 類(lèi)型)表示索引表的容量
?

?

? ? ? ?第一個(gè)0x0003,表示類(lèi)索引,全限定名是索引17指向的常量;第二個(gè)0x0004,表示父類(lèi)索引,全限定名是索引18指向的常量,即基類(lèi)Object;第三個(gè)0x0000,索引為0,就是上面所說(shuō)的,0的特殊情況,這里表示沒(méi)有接口索引集合,也就沒(méi)有實(shí)現(xiàn)任何接口。
字段表集合
? ? ? ?字段表用于描述接口或者類(lèi)中聲明的變量(包括類(lèi)變量和實(shí)例變量,但不包括方法內(nèi)部聲明的局部變量),字段能描述的信息有字段的作用域(public、protected和private修飾符)、是實(shí)例變量還是類(lèi)變量(static修飾符)、可變性(final)、并發(fā)可見(jiàn)性(volatile修飾符)、 可否被序列化(transient修飾符)、字段數(shù)據(jù)類(lèi)型(基本類(lèi)型、對(duì)象、數(shù)據(jù))、字段名稱(chēng),各個(gè)修飾符都是布爾值,字段數(shù)據(jù)類(lèi)型和名稱(chēng)無(wú)法固定,因此只能引用常量池的常量來(lái)描述
?

?

? ? ? ?上圖為字段表結(jié)構(gòu)和字段訪問(wèn)標(biāo)志,講字段表之前,再了解一下描述符。描述符用于描述字段的數(shù)據(jù)類(lèi)型、方法的參數(shù)列表(包括數(shù)量、類(lèi)型和順序)和返回值 基本類(lèi)型和void類(lèi)型都用一個(gè)大寫(xiě)字母來(lái)表示,對(duì)象用L加全限定名來(lái)表示。如下圖

? ? ? ?數(shù)組表示:每一維度使用一個(gè)前置的“[”表示,如 “java.lang.String[][]” 記錄為“[[Ljava/lang/String”; 一個(gè)整型數(shù)組 “int[]” 記錄為 “[I”。
? ? ? ?方法表示:先參數(shù)列表后返回值,如void inc()記錄為 “()V”,int indexOf(char[] source, int sourceOffset, int sourceCount,char[] target, int targetOffset, int targetCount,int fromIndex) 記錄為“([CII[CIII)I”。


? ? ? ?根據(jù)字段表結(jié)構(gòu),上圖畫(huà)出了每項(xiàng)的含義,第一項(xiàng)fields_count,表示后面字段表的數(shù)量,此處值為1,表示接下來(lái)只有一個(gè)字段表。這個(gè)字段表的access_flags為2,表示private,name_index為5,指向索引5,即字段名稱(chēng)為 m,descitor_index為6,指向索引6的描述符為I,即Integer類(lèi)型,最終的結(jié)果表示:private int m。在字節(jié)碼里,只要兩個(gè)字段的描述符不一致,就算重名也是合法的,這個(gè)編譯器編譯規(guī)則不一樣。接下來(lái)的attributes_count為0,表示沒(méi)有屬性表。
方法表集合


? ? ? ?方法表的描述與字段表的描述完全一致,結(jié)構(gòu)也是一致。

? ? ? ?methods_count值為2,可以想象到一個(gè)是實(shí)例構(gòu)造器init(),一個(gè)是inc()方法。根據(jù)字段表解析方法,就可以解析出這個(gè)方法是 public void inc()。
? ? ? ?疑問(wèn)???
? ? ? ?方法的定義可以通過(guò)訪問(wèn)標(biāo)志、名稱(chēng)索引、描述符索引表達(dá)清楚,但方法里面的 代碼去哪里了?
? ? ? ?答案:方法里的java代碼,經(jīng)過(guò)編譯器編譯成字節(jié)碼指令后,存放在方法屬性表集合中一個(gè)名為“Code”的屬性里
屬性表集合
? ? ? 接下來(lái)attributes_count值為1,表示當(dāng)前方法有1個(gè)屬性表。

? ? ? 在Class文件、字段表、方法表都可以攜帶自己的屬性表集合,用以描述某些場(chǎng)景專(zhuān)有的信息,對(duì)于每個(gè)屬性,名稱(chēng)需要從常量池中引用一個(gè)CONSTANT_utf8_info類(lèi)型的常量來(lái)表示,而屬性值的結(jié)構(gòu)則是完全自定義的,只需要通過(guò)一個(gè)u4的長(zhǎng)度屬性去說(shuō)明屬性值所占用的位數(shù)即可。
? ? ? Java虛擬機(jī)規(guī)范預(yù)定義了23種屬性。


? ? ? 繼續(xù)上面的屬性表,attribute_name_index值為0x0009,名稱(chēng)為Code,即Code屬性表,Code屬性表結(jié)構(gòu)如下圖

? ? ? java方法體中的代碼經(jīng)過(guò)javac編譯器處理后,最終變成字節(jié)碼指令存儲(chǔ)在Code屬性?xún)?nèi), 接口和抽象類(lèi)方法不存在Code屬性,如果一個(gè)程序中的信息分為代碼(Code,方法體內(nèi)的java代碼)和元數(shù)據(jù)(metadata, 包括類(lèi)、字段、方法定義及其他信息)兩部分,那么在Class文件中,Code屬性用于描述代碼,其他數(shù)據(jù)項(xiàng)目都用于描述元數(shù)據(jù)。
attribute_name_index: 指向CONSTANTS_utf8_info型常量型的索引,常量值固定為“Code”, 代表該屬性的屬性名稱(chēng)
attribute_length:表示屬性值得長(zhǎng)度,由于屬性名稱(chēng)索引和屬性長(zhǎng)度一共占6個(gè)字節(jié),那屬 性值的長(zhǎng)度固定為整個(gè)屬性表長(zhǎng)度減去6字節(jié)
max_stack: 操作數(shù)棧深度的最大值,方法執(zhí)行的任意時(shí)刻,操作數(shù)棧都不會(huì)超過(guò)這個(gè)深度, 虛擬機(jī)運(yùn)行時(shí)需根據(jù)這個(gè)值來(lái)分配棧幀中的操作棧深度
max_locals:局部變量表所需的存儲(chǔ)空間,單位是Slot(虛擬機(jī)為局部變量分配內(nèi)存所使用的最小單位),局部變量表中的Slot可以重用,當(dāng)代碼執(zhí)行超出一個(gè)局部變量的作用域時(shí),即被重用,javac編譯器根據(jù)作用域來(lái)分配Slot給各個(gè)變量使用,然后計(jì)算出max_locals的大小
code_length:字節(jié)碼長(zhǎng)度 code:字節(jié)碼指令的一系列字節(jié)流,一個(gè)指令是u1類(lèi)型,范圍為0x00~0xFF(0~255) exception_table_length:異常表長(zhǎng)度
exceptin_table:異常表,這里的異常是指代碼捕獲的異常
?

?

attribute_name_index: 0x0009,指向Code ? ? ? ? ? ? ? ? ?
attribute_length:0x0000001d,十進(jìn)制為29 ? ? ? ? ? ? ? ? ?
max_stack: 0x0001
max_locals:0x0001
code_length:0x00000005
code:0x2ab70001b1,字節(jié)碼指令
exception_table_length:0x0000,沒(méi)有異常信息
指令碼操作碼操作數(shù)描述(棧指操作數(shù)棧)
0x2aaload_0從局部變量0中裝載引用類(lèi)型值入棧
0xb7invokespecial0x00,?0x01編譯時(shí)方法綁定調(diào)用方法
0xb1return;void函數(shù)返回
?


? ? ? Code屬性是最重要的一個(gè)屬性,其他的屬性也可以按照這樣的思路繼續(xù)解析
總結(jié):
? ? ? 本文參照規(guī)范大概解析字節(jié)碼文件結(jié)構(gòu),字節(jié)碼文件主要是由一系列表集合組成,只是每個(gè)集合有各自含義,在描述集合前總會(huì)有一個(gè)容器計(jì)數(shù)器來(lái)說(shuō)明后面有多少個(gè)集合,以此劃分。由字節(jié)碼文件可知,所有類(lèi)文件信息,都存儲(chǔ)在常量池里,上面的所有描述,都是通過(guò)索引引用常量池常量。了解字節(jié)碼結(jié)構(gòu),對(duì)了解類(lèi)加載過(guò)程有一定的幫助。
參考文獻(xiàn)
[1] 周志明.深入理解Java虛擬機(jī).JVM高級(jí)特性與最佳實(shí)踐:機(jī)械工業(yè)出版社,2018.