很遺憾捺氢,這將是很枯燥的一章惜姐,但是如果想較為深入的理解JVM,這一章又很有必要硬著頭皮搞清楚胡岔。如果之前沒有接觸過類似的內(nèi)容法希,那么有很大的可能第一次基本讀不懂,如果出現(xiàn)這樣的情況也沒有關(guān)系姐军,請繼續(xù)保持學習,并且隔段時間再次重新閱讀尖淘。像我這樣不夠靈光的腦袋奕锌,學習了3遍也就能夠掌握基本原理。其實村生,只要掌握了對應的規(guī)則惊暴,類文件的內(nèi)容又是很容易解讀的,請保持你的耐心與好奇
Java號稱跨平臺趁桃,那么究竟是什么能夠使Java跨平臺辽话?簡單來說就是兩點:第一是編譯器能夠?qū)⒃创a編譯成某種平臺無關(guān)的格式;第二是能夠?qū)⒃摲N格式翻譯成具體平臺指令集的虛擬機
而這種平臺無關(guān)的格式就是字節(jié)碼卫病。虛擬機不與包括Java語言在內(nèi)的任何語言綁定油啤,它只與字節(jié)碼關(guān)聯(lián)。因此也就誕生了后續(xù)眾多基于JVM的新型語言
完整的類文件結(jié)構(gòu)說明請參考“官網(wǎng)文檔:The class File Format”
類文件結(jié)構(gòu)
1.數(shù)據(jù)組織方式:緊湊的二進制
類文件是一組以字節(jié)為基礎(chǔ)的二進制數(shù)據(jù)蟀苛,各數(shù)據(jù)項嚴格按照定義排列益咬,中間沒有分隔符及填充。如果遇到8位以上的數(shù)據(jù)項時帜平,則按照大端法(Big-Endian幽告,關(guān)于大端法可參考“理解字節(jié)序 - 阮一峰的網(wǎng)絡(luò)日志”)拆分成若干個字節(jié)存儲
2.數(shù)據(jù)類型:無符號數(shù)和表
#無符號數(shù)梅鹦,用來描述數(shù)字、索引或者UTF-8編碼的字符串值冗锁。u1齐唆、u2、u4冻河、u8分別代表1個字節(jié)箍邮、2個字節(jié)、4個字節(jié)芋绸、8個字節(jié)的無符號數(shù)
#表媒殉,由多個無符號數(shù)或其他表組合成復合數(shù)據(jù)結(jié)構(gòu)。Class文件本質(zhì)上就是一張表
3.多個同類數(shù)據(jù)項的描述:前置容量
由于類文件不采用分隔符的方式分隔數(shù)據(jù)摔敛,數(shù)據(jù)項的順序是被嚴格限定的廷蓉,因此當需要描述多個同類數(shù)據(jù)項的時候,采用前置容量計數(shù)器的方式
類文件結(jié)構(gòu)定義詳見下圖:
類文件結(jié)構(gòu)詳解
通過上面的講解马昙,我們對類文件結(jié)構(gòu)有了一個宏觀的了解桃犬。接下來,我們通過一個簡單的類文件實例行楞,來深入細節(jié)具體看一下類文件結(jié)構(gòu)
首先攒暇,我們定義一個足夠簡單的Java類,詳見下圖:
之后將該類編譯后子房,通過十六進制方式查看TestClass.class文件形用。看著像亂碼证杭?然而并不是
另外我們還可以通過javap命令田度,查看該文件的反匯編信息
1.魔數(shù)(magic)
魔數(shù)用來描述文件類型,是一個u4類型的數(shù)據(jù)(占據(jù)類文件的頭4個字節(jié))
Java類文件的魔數(shù)值是0xCAFEBABE解愤,看到這個是不是想起了Java的商標(咖啡)
使用魔數(shù)來表示文件類型镇饺,顯然比使用文件擴展名更加安全。虛擬機在讀取到0xCAFEBABE后則認為該文件是一個Class文件
2.版本號(version)
緊接著的4個字節(jié)代表的是類文件的版本號送讲,其中前兩個字節(jié)代表次版本號(Minor Version)奸笤,后兩個字節(jié)代表主版本號(Major Version)。通過版本號哼鬓,虛擬機能夠檢查是否可兼容該類文件
查看十六進制類文件监右,看到次版本號是0x0000(十進制0),主版本號是0x0034(十進制52)异希,我本地使用的編譯器版本是1.8.0秸侣。具體編譯器版本對應的十進制版本號請自行查閱,不在此贅述
3.常量池(constant_pool)
緊接著版本號的是常量池,常量池是類文件中第一個表類型的數(shù)據(jù)味榛,其中數(shù)據(jù)項眾多椭坚,并且數(shù)量不定。前面我們說過搏色,對于描述多個同類數(shù)據(jù)項的時候善茎,采用前置容量計數(shù)器的方式。因此在常量池之前频轿,是一個u2類型的數(shù)據(jù)垂涯,代表常量池容量計數(shù)(constant_pool_count)
查看TestClass類文件,常量池容量計數(shù)值是0x0016(十進制22)航邢,代表該類有21項常量耕赘,索引范圍是1-21(注意從1開始,而不是0)
常量池主要存放兩大類數(shù)據(jù):字面量和符號引用
#字面量比較接近于Java語言層面常量的概念膳殷,比如字符串操骡、聲明為final的常量值等
#符號引用主要包括:類和接口的全限定名、字段的名稱和描述符赚窃、方法的名稱和描述符册招。Java代碼編譯時,沒有“靜態(tài)連接”這一步驟勒极,而是通過“動態(tài)連接”的方式是掰。虛擬機在運行時,從常量池中獲取對應的符號引用辱匿,再翻譯到具體的內(nèi)存地址
常量池中每一各數(shù)據(jù)項都對應一個表键痛,一共有如下這些類型(14種),其中每一項開頭都包含一個u1類型的tag(下圖Value列)匾七,代表當前數(shù)據(jù)項代表的常量數(shù)據(jù)類型
下面我們繼續(xù)使用TestClass作為例子絮短,看看常量池中數(shù)據(jù)是怎樣定義的:
首先我們看到的tag值是0x0a(十進制10),查閱上表乐尊,看到對應的是CONSTANT_Methodref_info戚丸,說明該常數(shù)項是方法的符號引用划址。我們看一下CONSTANT_Methodref_info的數(shù)據(jù)定義:
第一項是tag扔嵌,上面說過了。第二項是class_index夺颤,代表擁有此方法的類的類信息在常量池中的索引痢缎。第三項是name_and_type_index,代表該方法的名稱和描述符信息在常量池中的索引
查看我們的類文件世澜,class_index值是0x0004独旷,說明常量池中第4項存放該類的類信息。name_and_type_index值是0x0012(十進制18),說明常量池中第18項存放名稱和描述符信息
另外嵌洼,從上面提到的反匯編信息中案疲,也可以更加明確地看出我們從十六進制類文件中分析出的內(nèi)容
上面,我們通過查閱CONSTANT_Methodref_info的數(shù)據(jù)定義麻养,并且對照十六進制類文件和反匯編信息褐啡,學習了怎樣讀懂類文件結(jié)構(gòu)中的常量池信息。其實其他類型的常量和CONSTANT_Methodref_info一樣鳖昌,都是類似的結(jié)構(gòu)备畦。下圖中選中的部分就是常量池相關(guān)的數(shù)據(jù),有興趣可以按照上述的方法對照官方文檔逐一進行解析
4.訪問標志(access_flags)
在常量池之后许昨,緊接著的兩個字節(jié)表示訪問標志懂盐。這個標志用于識別一些類或者接口層次的訪問信息。包括:該Class是否是public類型糕档、是否被聲明為final莉恼、是否是一個接口、是否是注解翼岁、是否是枚舉等
完整定義如下:
其中ACC_SUPER代表是否允許invokespecial指令的新語義类垫。invokespecial在JDK 1.0.2版本發(fā)生過改變,因此為了區(qū)分這條指令使用哪種語意琅坡,JDK 1.0.2之后該標志位都為0x0020悉患。對于1.8及以上版本,無論該標志位是否被設(shè)置榆俺,JVM都會統(tǒng)一認為該標志位為真
我們實例中的TestClass售躁,僅被定義為public,并且我當前使用的是1.8版本的JDK茴晋,因此ACC_PUBLIC及ACC_SUPER會被設(shè)置陪捷,其他標志位都為0。最終訪問標志位的值會被設(shè)置為0x0001 | 0x0020 = 0x0021
5.類索引(this_class)诺擅、父類索引(super_class)市袖、接口索引集合(interfaces)
類文件中通過這三項信息來確定這個類的繼承關(guān)系。其中類索引和父類索引都是u2類型的數(shù)據(jù)烁涌,接口索引集合是一組u2類型的數(shù)據(jù)(接口索引前會有一個u2類型數(shù)據(jù)表示接口索引的數(shù)量constant_pool_count)苍碟。這三類數(shù)據(jù)都指向常量池中的某項數(shù)據(jù)
類索引用來確定該類的全限定名认罩;父類索引確定其父類的全限定名躯枢。Java是單繼承,所以父類索引只有一個氓奈;接口索引集合用來描述該類實現(xiàn)了哪些接口
繼續(xù)看我們的TestClass抒钱,類索引值為0x0003(十進制3)蜓肆,說明類信息在常量池的第三項颜凯,結(jié)合反匯編代碼,可以看到“class_structure/TestClass”
父類索引值為0x0004(十進制4)仗扬,說明父類信息在常量池的第四項症概,結(jié)合反匯編代碼,可以看到TestClass繼承自“java/lang/Object”
接口索引的數(shù)量值為0x0000(十進制0)早芭,說明該類并沒有實現(xiàn)任何接口
6.字段表集合(fields)
字段表集合用于描述類中的變量(包括靜態(tài)變量穴豫、實例變量,但不包括局部變量)
每個字段通過一個field_info描述逼友,field_info格式定義如下:
field_info中的access_flags作用及計算方式與類的access_flags類似精肃,詳細定義如下:
access_flags之后是name_index和descriptor_index,分別代表字段的簡單名稱索引及描述符索引帜乞,他們都是對常量池中常量的引用司抱。之后是attributes方面的內(nèi)容,后面再做介紹
下面來看一下TestClass黎烈,fields_count值為0x0001(十進制1)习柠,代表只有一個字段(private int m;);access_flags值為0x0002(十進制2)照棋,對照上面的access_flags定義表资溃,發(fā)現(xiàn)只有ACC_PRIVATE為真,所以值為0x0002烈炭;name_index值為0x0005(十進制5)溶锭,說明字段的簡單名稱引用常量池中第5項;descriptor_index值為0x0006(十進制6)符隙,說明字段的描述符引用常量池中第6項(反匯編代碼常量第6項的“I”代表基本類型int)趴捅;attributes_count值為0x0000(十進制0),說明沒有額外屬性
7.方法表集合(methods)
顧名思義霹疫,方法表集合用于描述類中的方法
如果理解了上一節(jié)的字段表集合拱绑,那么方法表集合就很好理解了,因為method_info在結(jié)構(gòu)上與field_info極其類似丽蝎。每個方法都通過一個method_info來描述
同樣猎拨,第一項是access_flags,詳細定義如下:
后續(xù)幾項:name_index屠阻、descriptor_index红省、attributes含義都與字段表中類似,只不過在方法表中這些字段用于描述方法而已
也許你會有所疑問:方法里面的代碼在哪里栏笆?方法里面的代碼类腮,存放在屬性表集合中一個名為“Code”的屬性里臊泰。關(guān)于屬性表的內(nèi)容蛉加,后面我們再做講解
繼續(xù)回到TestClass蚜枢,methods_count值為0x0002(十進制2),代表有兩個方法(其中一個是編譯器自動添加的實例構(gòu)造器<init>针饥,另一個是我們自己定義的public int inc()方法)厂抽;第一個方法的access_flags值為0x0001(十進制1),對照上面的access_flags定義表丁眼,發(fā)現(xiàn)只有ACC_PUBLIC為真筷凤,所以值為0x0001;name_index值為0x0007(十進制7)苞七,說明方法名稱引用常量池中第5項藐守;descriptor_index值為0x0008(十進制8),說明方法的描述符引用常量池中第8項(反匯編代碼常量池第8項的“()V”代表void方法)蹂风;attributes_count值為0x0001(十進制1)卢厂,說明該方法的屬性表集合有一項屬性,索引為0x0009(十進制9)惠啄,對應常量池中第9項常量為“Code”慎恒,說明此屬性是方法的字節(jié)碼描述
8.屬性表集合(attributes)
前面在講解類文件、字段表撵渡、方法表時曾多次出現(xiàn)屬性表這個概念融柬,它的主要作用是用于描述某些場景下的專有信息
截止到j(luò)ava 8,屬性表集合中一共預定義了23種屬性趋距,下面我們拿一些屬性作為例子進行講解粒氧,完整的介紹請參看官方文檔
對于每個屬性,屬性的名稱(attribute_name_index)引用常量池中的常量节腐,屬性值(info)的結(jié)構(gòu)完全自定義靠欢,只需要一個u4類型的長度屬性(attribute_length)來說明屬性值占用的字節(jié)數(shù)
#Code屬性
前面在介紹方法表的時候曾提到過Code屬性,其用于存儲方法體中編譯后的內(nèi)容铜跑。但并非所有方法表都存在這個屬性门怪,比如接口和抽象類中的抽象方法。Code屬性結(jié)構(gòu)如下:
1)attribute_name_index和attribute_length上面已經(jīng)講過
2)max_stack代表操作數(shù)棧的最大深度锅纺,虛擬機需要根據(jù)這個值來分配棧幀中操作數(shù)棧的深度
3)max_locals代表了局部變量表所需的存儲空間掷空。max_locals的單位是slot,slot是虛擬機為局部變量分配內(nèi)存的最小單位囤锉。對于32位的數(shù)據(jù)類型(byte坦弟、char、short官地、int酿傍、float、boolean驱入、returnAddress)赤炒,每個局部變量占用一個slot氯析,而對于64位的數(shù)據(jù)類型(long、double)則需要占用兩個slot莺褒。另外掩缓,max_locals的值并不是方法中定義了多少個局部變量,就把相應占用的slot數(shù)量簡單相加遵岩。原因在于你辣,當代碼執(zhí)行超出了某個變量的作用域之后,它所占用的slot就可以被其他的局部變量所占用尘执,因此slot實際是可以復用的舍哄。編譯器會根據(jù)作用域給本地變量分配slot,然后計算出max_locals的值
4)code_length和code用來存儲編譯器編譯后的字節(jié)碼指令(類似于匯編指令)誊锭。code_length代表字節(jié)碼的長度蠢熄,code則是一系列字節(jié)碼流。每個字節(jié)碼指令占用一個字節(jié)炉旷,當虛擬機讀取到code中的一個字節(jié)签孔,會根據(jù)字節(jié)碼指令表找到對應的指令,并且可以知道這條指令后面是否會跟隨參數(shù)窘行,以及參數(shù)數(shù)量和具體含義饥追。1個字節(jié)取值范圍是0x00(十進制0)~0xFF(十進制255),也就是說一共可以表示256種指令
下面我們再次通過我們的TestClass來看一下code_length和code是如何定義的罐盔。首先code_length值為0x00000005(十進制5)但绕,說明后續(xù)五個字節(jié)是code
code中第一個字節(jié)值是0x2A,查表得知對應指令為aload_0惶看,該指令含義是將第0個slot中的引用類型的本地變量推入操作數(shù)棧頂
code中第二個字節(jié)值是0xB7捏顺,查表得知對應指令為invokespecial,該指令含義是將操作數(shù)棧頂?shù)囊脭?shù)據(jù)所指向的對象作為方法接收者纬黎,調(diào)用該對象的實例構(gòu)造方法幅骄、private方法或者他父類的方法
code中第三和第四個字節(jié)值是0x0001(十進制1),這個u2類型的數(shù)據(jù)是前面invokespecial指令的參數(shù)本今,它指向常量池中第一個常量拆座,代表具體調(diào)用哪個方法。查看反匯編代碼冠息,可以看到對應的是“java/lang/Object."<init>":()V”挪凑,代表調(diào)用父類Object的實例構(gòu)造方法
code中第五個字節(jié)值是0xB1,查表得知對應指令為return逛艰,含義是從當前方法返回void躏碳,這條指令執(zhí)行后方法結(jié)束
我們再次查看TestClass的反匯編代碼,看到其中兩個方法(實例構(gòu)造方法和inc方法)args_size的值都是1散怖,但是這兩個方法實際上都是無參的肄渗。另外無論是參數(shù)列表還是方法體內(nèi),都沒有定義任何局部變量脸甘,但是locals也都是1沿猜。這是因為橄妆,在實例方法內(nèi),我們可以通過this關(guān)鍵字訪問此方法所屬的對象剃袍,而this正是通過編譯器在方法調(diào)用時通過方法參數(shù)自動傳入的。如果inc方法是static的虱肄,那么args_size就是0了
5)exception_table_length和exception_table用來描述異常處理信息。exception_table中一共包含4項信息御毅,含義是:如果在start_pc到end_pc(不含)位置出現(xiàn)了類型為catch_type(包含其子類)的異常根欧,則轉(zhuǎn)向handler_pc處進行處理
#Exceptions屬性
這里的Exceptions屬性與上面講到的Code屬性里的exception_table不是一回事兒,這里的Exceptions屬性與Code屬性平級端蛆,代表該方法可能拋出的checked異常
Exceptions屬性中的number_of_exceptions表示方法可能拋出的checked異常的數(shù)量凤粗。exception_index_table指向常量池中的常量,表示異常類型
#LineNumberTable屬性
LineNumberTable屬性用于描述源代碼行號與字節(jié)碼偏移量之間的對應關(guān)系今豆。它雖然不是運行時必須的屬性嫌拣,但是如果沒有相應的信息,那么程序拋出異常時呆躲,異常堆棧中將沒有行號亭罪,另外也無法按照源碼行來設(shè)置斷點
#LocalVariableTable屬性
LocalVariableTable屬性用于描述棧幀中局部變量表中變量與源碼中變量的關(guān)系。它也不是運行時必須的屬性歼秽,但是如果沒有相應信息应役,那么當其他人引用方法時,源碼中定義的參數(shù)名稱都將丟失燥筷,取而代之的是類似arg0箩祥、arg1這樣的的占位符
#Signature屬性
Signature屬性出現(xiàn)于類、字段表肆氓、方法表結(jié)構(gòu)的屬性中袍祖,用于JDK1.5之后,記錄范型信息谢揪。之所以加入一個屬性記錄范型信息蕉陋,是因為Java中范型采用的是擦除法實現(xiàn)的偽范型。在字節(jié)碼中拨扶,范型信息會被擦除凳鬓,優(yōu)點是實現(xiàn)簡單(主要修改編譯器,虛擬機很少改動)患民,但缺點就是運行期間無法獲得范型信息缩举。Signature屬性就是為了彌補這個缺陷增設(shè)的
行文至此,我想類文件的結(jié)構(gòu)原理已經(jīng)基本描述清楚了,其余沒有講到的結(jié)構(gòu)也都是類似仅孩,如果有興趣或者日后有需要用到托猩,可以再做詳細的了解
筆記3結(jié)束