最近在寫一個私人項目,名字叫作ClassAnalyzer
,ClassAnalyzer
的目的是能讓咱們對Java Class
文件的設計與結構可以有一個深刻的理解。主體框架與基本功能已經完成,還有一些細節功能往後再增長。實際上JDK
已經提供了命令行工具javap
來反編譯Class
文件,但本篇文章將闡明我實現解析器的思路。java
做爲類或者接口信息的載體,每一個Class
文件都完整的定義了一個類。爲了使Java
程序能夠「編寫一次,到處運行」,Java虛擬機規範對Class
文件進行了嚴格的規定。構成Class
文件的基本數據單位是字節,這些字節之間不存在任何分隔符,這使得整個Class
文件中存儲的內容幾乎所有是程序運行的必要數據,單個字節沒法表示的數據由多個連續的字節來表示。git
根據Java
虛擬機規範,Class
文件採用一種相似於C
語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表。Java
虛擬機規範定義了u1
、u2
、u4
和u8
來分別表示1
個字節、2
個字節、4
個字節和8
個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量值或者是字符串。表是由多個無符號數或者其它表做爲數據項構成的複合數據類型,表用於描述有層次關係的複合結構的數據,所以整個Class
文件本質上就是一張表。在ClassAnalyzer
中u1
、u2
、u4
和u8
分別對應於byte
、short
、int
和long
,Class
文件被描述爲以下Java
類。github
public class ClassFile {
public U4 magic; // magic
public U2 minorVersion; // minor_version
public U2 majorVersion; // major_version
public U2 constantPoolCount; // constant_pool_count
public ConstantPoolInfo[] cpInfo; // cp_info
public U2 accessFlags; // access_flags
public U2 thisClass; // this_class
public U2 superClass; // super_class
public U2 interfacesCount; // interfaces_count
public U2[] interfaces; // interfaces
public U2 fieldsCount; // fields_count
public FieldInfo[] fields; // fields
public U2 methodsCount; // methods_count
public MethodInfo[] methods; // methods
public U2 attributesCount; // attributes_count
public BasicAttributeInfo[] attributes; // attributes
}複製代碼
組成Class
文件的各個數據項中,例如魔數、Class
文件的版本、訪問標誌、類索引和父類索引等數據項,它們在每一個Class
文件中都佔用固定數量的字節,在解析時只須要讀取相應數量的字節。除此以外,須要靈活處理的主要包括4
部分:常量池、字段表集合、方法表集合和屬性表集合。字段和方法均可以具有本身的屬性,Class
自己也有相應的屬性,所以,在解析字段表集合和方法表集合的同時也包含了屬性表的解析。框架
常量池佔據了Class
文件很大一部分的數據,用於存儲全部的常量信息,包括數字和字符串常量、類名、接口名、字段名和方法名等。Java
虛擬機規範定義了多種常量類型,每一種常量類型都有本身的結構。常量池自己是一個表,在解析時有幾點須要注意。工具
u1
類型的tag
來標識。constantPoolCount
)比實際大1
,例如,若是constantPoolCount
等於47
,那麼常量池中有46
項常量。1
開始,例如,若是constantPoolCount
等於47
,那麼常量池的索引範圍爲1~46
。設計者將第0
項空出來的目的是用於表達「不引用任何一個常量池項目」。CONSTANT_Utf8_info
型常量的結構中包含u1
類型的tag
、u2
類型的length
和由length
個u1
類型組成的bytes
,這length
字節的連續數據是一個使用MUTF-8
(Modified UTF-8)
編碼的字符串。MUTF-8
與UTF-8
並不兼容,主要區別有兩點:一是null
字符會被編碼成2
字節(0xC0
和0x80
);二是補充字符是按照UTF-16
拆分爲代理對分別編碼的,相關細節能夠看這裏(變種UTF-8)。屬性表用於描述某些場景專有的信息,Class
文件、字段表和方法表都有相應的屬性表集合。Java
虛擬機規範定義了多種屬性,ClassAnalyzer
目前實現了對經常使用屬性的解析。和常量類型的數據項不一樣,屬性並無一個tag
來標識屬性的類型,可是每一個屬性都包含有一個u2
類型的attribute_name_index
,attribute_name_index
指向常量池中的一個CONSTANT_Utf8_info
類型的常量,該常量包含着屬性的名稱。在解析屬性時,ClassAnalyzer
正是經過attribute_name_index
指向的常量對應的屬性名稱來得知屬性的類型。this
字段表用於描述類或者接口中聲明的變量,字段包括類級變量以及實例級變量。字段表的結構包含一個u2
類型的access_flags
、一個u2
類型的name_index
、一個u2
類型的descriptor_index
、一個u2
類型的attributes_count
和attributes_count
個attribute_info
類型的attributes
。咱們已經介紹了屬性表的解析,attributes
的解析方式與屬性表的解析方式一致。編碼
Class
的文件方法表採用了和字段表相同的存儲格式,只是access_flags
對應的含義有所不一樣。方法表包含着一個重要的屬性:Code
屬性。Code
屬性存儲了Java
代碼編譯成的字節碼指令,在ClassAnalyzer
中,Code
對應的Java
類以下所示(僅列出了類屬性)。spa
public class Code extends BasicAttributeInfo {
private short maxStack;
private short maxLocals;
private long codeLength;
private byte[] code;
private short exceptionTableLength;
private ExceptionInfo[] exceptionTable;
private short attributesCount;
private BasicAttributeInfo[] attributes;
...
private class ExceptionInfo {
public short startPc;
public short endPc;
public short handlerPc;
public short catchType;
...
}
}複製代碼
在Code
屬性中,codeLength
和code
分別用於存儲字節碼長度和字節碼指令,每條指令即一個字節(u1
類型)。在虛擬機執行時,經過讀取code
中的一個個字節碼,並將字節碼翻譯成相應的指令。另外,雖然codeLength
是一個u4
類型的值,可是實際上一個方法不容許超過65535
條字節碼指令。命令行
ClassAnalyzer
的源碼已放在了GitHub上。在ClassAnalyzer
的README中,我以一個類的Class
文件爲例,對該Class
文件的每一個字節進行了分析,但願對你們的理解有所幫助。翻譯