《深入理解Java 虛擬機(jī)》之類文件結(jié)構(gòu)

JVM多語言支持

Java規(guī)范分為Java語言規(guī)范(The Java Language Specification)和Java虛擬機(jī)規(guī)范(The Java Virtual Machine Specification),因此JVM支持多種語言,只要該語言編譯后的類文件符合JVM規(guī)范。比如我們常用的Scala、Kotlin、Clojure、Groovy等等。

類文件結(jié)構(gòu)

基礎(chǔ)原則:多字節(jié)的數(shù)據(jù),高位在前。JVM加載Class文件的時(shí)候進(jìn)行動態(tài)連接。
Class文件結(jié)構(gòu)類似C的結(jié)構(gòu)體,包含無符號數(shù)(u1/u2/u4/u8表示1/2/4/8字節(jié)的無符號數(shù))和表(由多個(gè)無符號數(shù)或表組成的結(jié)構(gòu)體,class文件本身就是一個(gè)大的表),有多個(gè)同類的無符號數(shù)或者表并數(shù)量不確定的時(shí)候,一般先用一個(gè)無符號數(shù)記錄數(shù)量,后面接上一系列連續(xù)的這種無符號數(shù)或者表。Class文件沒有分隔符號,所以整個(gè)數(shù)據(jù)結(jié)構(gòu)都是被嚴(yán)格規(guī)定的。


image

魔數(shù)與Class文件版本

Class文件頭4個(gè)字節(jié)是固定的0xCAFEBABE(咖啡寶貝),顯然與Java語言命名的歷史相關(guān)。
緊接著4個(gè)字節(jié)存儲Class文件版本號,5-6字節(jié)是子版本號,7-8字節(jié)是主版本號(比如1.7.0是0x0033)。JVM讀取Class文件的時(shí)候,搞版本JDK可以兼容舊版本Class文件,就是通過這4個(gè)字節(jié)進(jìn)行判定的。

常量池

一般來說常量池占Class文件空間最大,由于長度不定,所以入口有u2類型的常量池容量計(jì)數(shù)器(8-9位),計(jì)數(shù)從1開始(Class文件中其他容量計(jì)數(shù)器都是從0開始的)。
常量池存儲兩類常量:字面量(類似Java語言中的常量)和符號引用,后者包括類和接口全名、字段名稱和描述符、方法名稱和描述符。JVM運(yùn)行時(shí)從常量池獲取符號引用再在類創(chuàng)建時(shí)解析到具體內(nèi)存地址,沒有C語言的“連接”步驟。
常量池每一個(gè)常量都是一個(gè)表。,一共有14種表,表的開頭都是一個(gè)u1類型的標(biāo)志位代表當(dāng)前常量的類型(浮點(diǎn)整形之類),后面的結(jié)構(gòu)與具體的常量類型有關(guān),各自不同。
使用javap -verbose 類名可以解析類的結(jié)構(gòu),輸出結(jié)構(gòu)大概這樣:

 javap -verbose com.turingdi.breorent.user.controller.RentAndReturnController

Classfile /home/leibniz/workspace/BreoRent/target/classes/com/turingdi/breorent/user/controller/RentAndReturnController.class
  Last modified 2017-6-6; size 9126 bytes
  MD5 checksum 26ed5594f39cfc9d6b109637ad76bf12
  Compiled from "RentAndReturnController.java"
public class com.turingdi.breorent.user.controller.RentAndReturnController
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Methodref          #111.#203     // java/lang/Object."<init>":()V
    #2 = Class              #204          // org/springframework/web/servlet/ModelAndView
    #3 = Methodref          #2.#203       // org/springframework/web/servlet/ModelAndView."<init>":()V
    #4 = Class              #205          // com/turingdi/breorent/common/wechatApi/process/WechatJdk
    #5 = Methodref          #4.#206       // com/turingdi/breorent/common/wechatApi/process/WechatJdk."<init>":(Ljavax/servlet/http/HttpServletRequest;)V
    #6 = Methodref          #4.#207       // com/turingdi/breorent/common/wechatApi/process/WechatJdk.getMap:()Ljava/util/Map;
    #7 = String             #208          // appId
    #8 = String             #209          // wechat
    #9 = String             #210          // APP_ID
……

常見的兩種常量舉例:

  1. CONSTANT_Class_info,類常量,標(biāo)志為0x07,緊接著是1個(gè)u2類型的類名索引,指的是類名(字符串常量)在常量池中的索引(如上面所說,從1開始數(shù))。
  2. CONSTANT_Utf8_info,字符串變量,標(biāo)志為0x01,緊接著是1個(gè)u2類型的字符串長度,然后是字符串內(nèi)容的bytes,u1類型,數(shù)量等于前面u2定義的。

從上面可以推導(dǎo):類名(全限定名)是字符串常量,長度用u2類型表示,也就是最大長度是65535,換言之就是Java類全名最長65535,超過的無法編譯。

訪問標(biāo)志

常量池結(jié)束之后,有兩個(gè)字節(jié)為訪問標(biāo)志,代表當(dāng)前Class文件是否public、是否類/接口/枚舉、是否抽象、是否注解等等。

類索引、父類索引及接口索引集合

類索引父類索引分別為u2類型數(shù)據(jù),接口索引集合結(jié)合為u2類型數(shù)據(jù)的集合(只能有一個(gè)父類,可以實(shí)現(xiàn)多個(gè)接口),分別用于記錄當(dāng)前類、父類、實(shí)現(xiàn)的接口的類描述符(CONSTANT_Class_info)在常量池中的索引。
類索引和父類索引緊接在訪問標(biāo)志后面,再后面是接口索引集合,入口是一個(gè)數(shù)量計(jì)數(shù)器,0表示沒有實(shí)現(xiàn)任何接口,再后面就是具體的接口類描述符索引。

字段表集合

描述類或接口中定義的字段,包括靜態(tài)和非靜態(tài)的。結(jié)構(gòu)如下:

類型 名稱 數(shù)量 含義
u2 access_flags 1 訪問標(biāo)志,public/private/final/static/enum等描述符
u2 name_index 1 字段簡單名稱在常量池中的索引,即變量名或方法名
u2 descriptor_index 1 字段和方法的描述符在常量池的索引,描述字段類型或方法參數(shù)列表/返回類型
u2 attibutes_count 1 屬性表計(jì)數(shù)器
attribute_info attributes attibutes_count 屬性額外描述,比如描述變量初始化值在常量池中的索引

描述符描述方法的時(shí)候,先是參數(shù)列表,然后是返回值類型。而方法

public String toString(int test)

對應(yīng)的描述符是

(I)Ljava/lang/String;

其中L是表示對象類型。
此外,字段表集合不會列出從父類或接口中繼承的字段,但可能會有代碼中不存在的字段,比如內(nèi)部類對外部類實(shí)例的引用之類。

方法表集合

方法表集合的入口同樣也是一個(gè)u2類型的計(jì)數(shù)器,緊接著是各個(gè)具體的方法。方法表的結(jié)構(gòu)與字段表基本一樣,不列出來了。區(qū)別:

  1. 首先是access_flags的取值范圍不同,比如沒有ACC_TRANSIENT、有ACC_SYNCHRONIZED等值;
  2. name_index表示方法名索引,descriptor_index表示方法描述符索引,跟字段表一樣;
  3. 而編譯后的方法代碼,放在屬性表里面名為“Code”的屬性中;
  4. 沒有Override的父類方法,不會出現(xiàn)在子類的方法表集合中;
  5. 同樣可能出現(xiàn)代碼中原本沒有的方法,比如<clinit>(類構(gòu)造器)、<init>(實(shí)例對象構(gòu)造器)。

兩個(gè)方法名字相同,參數(shù)列表相同,返回值類型不同,是允許共存在一個(gè)Class文件中的,但Java語言不允許這樣。

屬性表集合

Class文件、字段表、方法表都可以有自己的屬性表,Java7里面定義了21種屬性。

Code屬性

并非所有方法表都有Code屬性,比如接口和抽象類的方法就沒有。結(jié)構(gòu)如下:

類型 名稱 數(shù)量 含義
u2 attribute_name_index 1 屬性名的索引,對Code屬性而言恒為”Code”
u4 attribute_length 1 屬性值長度,相當(dāng)于整個(gè)屬性表長度長度減6(u2+u4)
u2 max_stack 1 操作數(shù)棧深度最大值。JVM運(yùn)行時(shí)根據(jù)此值分配棧楨的操作棧深度
u2 max_locals 1 局部變量表所需存儲空間,單位是Slot,double和long占用2個(gè)Slot、其他基本類型1Slot,Slot空間可以重用(變量作用域問題)
u4 code_length 1 編譯后的字節(jié)碼長度,理論上最長2^32-1,實(shí)際上JVM規(guī)定一個(gè)方法不允許超過65535條字節(jié)碼指令
u1 code code_length 代碼編譯后的字節(jié)碼
u2 exception_table_length 1 異常表長度
exception_info exception_table exception_table_length 異常表,記錄字節(jié)碼在start_pc到end_pc行之間如果出現(xiàn)類型為catch_type或其子類的異常則跳轉(zhuǎn)到handler_pc行繼續(xù)處理
u2 attibutes_count 1 屬性表計(jì)數(shù)器
attribute_info attributes attibutes_count 屬性額外描述,比如描述變量初始化值在常量池中的索引

字節(jié)碼值得注意的一個(gè)地方是,javac編譯時(shí)將this關(guān)鍵字作為一個(gè)普通方法參數(shù)由JVM調(diào)用時(shí)自動傳入。

Exceptions屬性

描述方法可能拋出的受檢異常。

LineNumberTable屬性

描述Java遠(yuǎn)嗎行號與字節(jié)碼行號之間映射關(guān)系,也就是為什么拋異常的時(shí)候可以顯示源碼哪一行拋出的。

LocalVariableTable屬性

描述棧楨中局部變量表與Java源碼中變量的關(guān)系,以保證編譯后的代碼被其他代碼調(diào)用時(shí),IDE可以顯示參數(shù)名(否則被arg0、arg1之類的變量名代替)

SourceFile屬性

描述生成當(dāng)前Class文件的源文件名稱,也是拋異常時(shí)可以顯示源文件名字的原因。但內(nèi)部類不會生成這個(gè)屬性。

ConstantValue屬性

static關(guān)鍵字修飾的變量可以使用這個(gè)屬性。對于Sun javac編譯器,final static的變量采用ConstantValue屬性初始化,其他static變量在<clinit>(類構(gòu)造器)中初始化。

InnerClasses屬性

記錄內(nèi)部類和宿主類的關(guān)聯(lián)。內(nèi)部類和宿主類的Class文件都會有這個(gè)屬性。

Signature屬性

記錄泛型簽名信息。Java的泛型是使用擦除式實(shí)現(xiàn)的偽泛型,編譯后擦除泛型,這個(gè)屬性為了彌補(bǔ)此缺陷,方便反射API可以拿到泛型類型。

字節(jié)碼指令

字節(jié)碼指令由一個(gè)字節(jié)的操作碼(代表具體操作)及跟隨其后的0個(gè)或多個(gè)操作數(shù)(操作所需的參數(shù))組成。JVM大多數(shù)指令不含操作數(shù)只有操作碼。
Class文件放棄了操作數(shù)對齊,因此省略很多填充和分割符,因此體積可以盡量小;缺點(diǎn)是損失一些解析字節(jié)碼的性能。
JVM的指令大多數(shù)包含了操作的數(shù)據(jù)類型信息,但因?yàn)橹挥幸粋€(gè)字節(jié),也就是說最多只有256種指令,所以不是所有命令對所有數(shù)據(jù)類型都有獨(dú)立的指令(非完全獨(dú)立),同時(shí)提供一些指令將指令不支持類型的操作數(shù)轉(zhuǎn)換為可支持的類型。
JVM的浮點(diǎn)數(shù)運(yùn)算,舍入模式是最低有效位向下(0)取整。操作溢出時(shí)用有符號的無窮大表示(INF),如果操作結(jié)果沒有明確數(shù)學(xué)意義則得到NaN(非數(shù)字,比如0/0,∞×0之類)

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

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

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