一、概述
各種不同平台的Java虛擬機, 以及所有平台都統一支持的程序存儲格式——字節碼(Byte Code)是構成平台無關性的基石,所以class文件主要用於解決平台無關性的中間文件。如下圖所示:

java虛擬機不與包括Java語言在內的任何程序語言綁定, 它只與“Class文件”這種特定的二進制文件格式所關聯, Class文件中包含了Java虛擬機指令集、 符號表以及若干其他輔助信息。
每一個class文件都對應着唯一一個類或者接口的定義信息,但是相對地,類或者接口並不一定都必須定義在文件里(比如類或者接口也可以通過類加載器直接生成)
每個class文件都是由字節流組成,各個數據項目嚴格按照順序緊湊地排列在文件之中, 中間沒有添加任何分隔符,每個字節流含有8個二進制位,所有的16位,32位和64位長度的數據將通過2個,4個和8個連續的8位字節來對其進行表示,多字節數據總是按照big-endian(大端在前:也就是說高位字節存儲在低的地址上面,而低位字節存儲到高地址上面)的順序進行存儲,在Java JDK中,可以使用java.io.DataInput、java.io.DataOutput等接口和java.io.DataInputStream和java.io.DataOutputStream等類來訪問這種格式的數據Class文件結構采用類似C語言的結構體來存儲數據的。
Class文件格式采用一種類似於C語言結構體的偽結構來存儲數據,主要有兩類數據項,無符號數和表,無符號數用來表述數字,索引引用以及字符串等,比如 u1,u2,u4,u8分別代表1個字節,2個字節,4個字節,8個字節的無符號數,而表是任意數量的可變長項組成,是有多個無符號數以及其它的表組成的復合結構,所有表的命名都習慣性地以“_info”結尾,無論是無符號數還是表, 當需要描述同一類型但數量不定的多個數據時, 經常會使用一個前置的容量計數器加若干個連續的數據項的形式, 這時候稱這一系列連續的某一類型的數據為某一類型的“集合”。
二、Class類文件的結構
| 類型 | 名稱 | 數量 |
|---|---|---|
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count-1 |
| u2 | access_flags | 1 |
| u2 | this_class | 1 |
| u2 | super_class | 1 |
| u2 | interfaces_count | 1 |
| u2 | interfaces | interfaces_count |
| u2 | fields_count | 1 |
| field_info | fields | fields_count |
| u2 | methods_count | 1 |
| method_info | methods | methods_count |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
2.1、魔數和java版本號
每個Class文件的頭4個字節被稱為魔數(Magic Number) , 它的唯一作用是確定這個文件是否為一個能被虛擬機接受的Class文件。Class文件的魔數取得很有“浪漫氣息”,
值為0xCAFEBABE(咖啡寶貝? )
緊接着魔數的4個字節存儲的是Class文件的版本號: 第5和第6個字節是次版本號(MinorVersion) , 第7和第8個字節是主版本號(Major Version)
這里我們使用一個簡單的代碼進行分析:
public class TestClass { private int m; public int inc() { return m + 1; } }
使用javac命令對其進行編譯,並使用WinHex (下載地址:http://www.x-ways.net/winhex/index-m.html)工具打開,得到如下的圖,前面幾位就是魔數和版本號

這里可以得出我們使用的版本為java1.8,16進制的34等於10進制的52

2.2、常量池
緊接着主、 次版本號之后的是常量池入口, 常量池可以比喻為Class文件里的資源倉庫, 它是Class文件結構中與其他項目關聯最多的數據, 通常也是占用Class文件空間最大的數據項目之一, 另外, 它還是在Class文件中第一個出現的表類型數據項目 ,常量池的入口需要放置一項u2類型的數據, 代表常量池容量計數值(constant_pool_count) ,這個容量計數是從1開始的。如下圖所示:常量池容量(偏移地址: 0x00000008) 為十六進制數0x0013,則十進制為19,則這里有18個長常量,索引范圍為1-18,在Class文件格式規范制定之時, 設計者將第0項常量空出來是有特殊考慮的, 這樣做的目的在於, 如果后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義, 可以把索引值設置為0來表示。

然后我們使用javap命令查看該class文件:(這里明顯顯示為18個常量)

常量池中主要存放兩大類常量: 字面量(Literal) 和符號引用(Symbolic References) 。
字面量比較接近於Java語言層面的常量概念, 如文本字符串、 被聲明為final的常量值等。
符號引用則屬於編譯原理方面的概念, 主要包括下面幾類常量:
- 被模塊導出或者開放的包(Package)
- 類和接口的全限定名(Fully Qualified Name)
- 字段的名稱和描述符(Descriptor)
- 方法的名稱和描述符
- 方法句柄和方法類型(Method Handle、 Method Type、 Invoke Dynamic)
- 動態調用點和動態常量(Dynamically-Computed Call Site、 Dynamically-Computed Constant)
虛擬機在加載Class文件時才會進行動態連接,也就是說,Class文件中不會保存各個方法、 字段最終在內存中的布局信息, 這些字段、 方法的符號引用不經過虛擬機在運行期轉換的話是無法得到真正的內存入口地址, 也就無法直接被虛擬機使用的,當虛擬機做類加載時, 將會從常量池獲得對應的符號引用, 再在類創建時或運行時解析、 翻譯到具體的內存地址之中常量池中每一項常量都是一個表,截至JDK13, 常量表中分別有17種不同類型的常量。這17類表都有一個共同的特點, 表結構起始的第一位是個u1類型的標志位,代表着當前常量屬於哪種常量類型。 17種常量類型所代表的具體含義如下圖所示。
符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標並不一定已經加載到了內存中。
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在於內存之中了
| 類型 |
項目 |
類型 |
描述 |
| CONSTANT_Utf8_info |
tag |
u1 |
值為1 |
| length |
u2 |
utf-8縮略編碼字符串占用字節數 |
|
| bytes |
u1 |
長度為length的utf-8縮略編碼字符串 |
|
| CONSTANT_Integer_info |
tag |
u1 |
值為3 |
| bytes |
u4 |
按照高位在前儲存的int值 |
|
| CONSTANT_Float_info |
tag |
u1 |
值為4 |
| bytes |
u4 |
按照高位在前儲存的float值 |
|
| CONSTANT_Long_info |
tag |
u1 |
值為5 |
| bytes |
u8 |
按照高位在前儲存的long值 |
|
| CONSTANT_Double_info |
tag |
u1 |
值為6 |
| bytes |
u8 |
按照高位在前儲存的double值 |
|
| CONSTANT_Class_info |
tag |
u1 |
值為7 |
| index |
u2 |
指向全限定名常量項的索引 |
|
| CONSTANT_String_info |
tag |
u1 |
值為8 |
| index |
u2 |
指向字符串字面量的索引 |
|
| CONSTANT_Fieldref_info |
tag |
u1 |
值為9 |
| index |
u2 |
指向聲明字段的類或接口描述符CONSTANT_Class_info的索引項 |
|
| index |
u2 |
指向字段描述符CONSTANT_NameAndType_info的索引項 |
|
| CONSTANT_Methodref_info |
tag |
u1 |
值為10 |
| index |
u2 |
指向聲明方法的類描述符CONSTANT_Class_info的索引項 |
|
| index |
u2 |
指向名稱及類型描述符CONSTANT_NameAndType_info的索引項 |
|
| CONSTANT_InterfaceMethodref_info |
tag |
u1 |
值為11 |
| index |
u2 |
指向聲明方法的接口描述符CONSTANT_Class_info的索引項 |
|
| index |
u2 |
指向名稱及類型描述符CONSTANT_NameAndType_info的索引項 |
|
| CONSTANT_NameAndType_info
|
tag |
u1 |
值為12 |
| index |
u2 |
指向該字段或方法名稱常量項的索引 |
|
| index |
u2 |
指向該字段或方法描述符常量項的索引 |
|
| CONSTANT_MethodHandle_info | tag |
u1 |
值為15 |
| refrence_kind | u1 | 值必須在1-9之間,決定了方法句柄的類型,方法句柄的類型的值表示方法句柄字節碼的行為 | |
| refrence_index | u2 | 值必須是對常量池的有效索引 | |
| CONSTANT_MethodType_info | tag | u1 | 值為16 |
| descriptor_index | u2 | 值必須對常量池的有效索引,常量池在該處的項必須是CONSTANT_Utf8_info表示方法的描述符 | |
| CONSTANT_Dynamic_info | tab | u1 | 值為17 |
| bootstrap_method_attr_index | u2 | 值必須對當前Class文件中引導方法表的bootstrap_methods[]數組的有效索引 | |
| name_and_type_index | u2 | 值必須對當前常量池的有效索引,常量池中在該索引出的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 | |
| CONSTANT_InvokeDynamic_info | tag | u1 | 值為18 |
| bootstrap_method_attr_index | u2 | 值必須對當前Class文件中引導方法表的bootstrap_methods[]數組的有效索引 | |
| name_and_type_index | u2 | 值必須對當前常量池的有效索引,常量池中在該索引出的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 | |
| CONSTANT_Module_info | tag | u1 | 值為19 |
| name_index | u2 | 值必須對常量池的有效索引,常量池在該處的項必須是CONSTANT_Utf8_info表示模塊名 | |
| CONSTANT_Package_info | tag | u1 | 值為20 |
| name_index | u2 | 值必須對常量池的有效索引,常量池在該處的項必須是CONSTANT_Utf8_info表示包名 |
2.3、訪問標志
在常量池結束之后,緊接着的兩個字節代表訪問標志(access_flags),這個標志用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract類型,如果是類的話,是否被聲明為final等,具體的標志位以及標志的含義如下:
| 字段的訪問權限 |
||
| Flag Name |
Value |
Remarks |
| ACC_PUBLIC |
0x0001 |
pubilc,包外可訪問。 |
| ACC_PRIVATE |
0x0002 |
private,只可在類內訪問。 |
| ACC_PROTECTED |
0x0004 |
protected,類內和子類中可訪問。 |
| ACC_STATIC |
0x0008 |
static,靜態。 |
| ACC_FINAL |
0x0010 |
final,常量。 |
| ACC_VOILATIE |
0x0040 |
volatile,直接讀寫內存,不可被緩存。不可和ACC_FINAL一起使用。 |
| ACC_TRANSIENT |
0x0080 |
transient,在序列化中被忽略的字段。 |
| ACC_SYNTHETIC |
0x1000 |
synthetic,由編譯器產生,不存在於源代碼中。 |
| ACC_ENUM |
0x4000 |
enum,枚舉類型字段 |
| ACC_MODULE |
0x8000 |
標識這是一個模塊 |
2.4、類索引、 父類索引與接口索引集合
類索引(this_class) 和父類索引(super_class) 都是一個u2類型的數據, 而接口索引集合(interfaces) 是一組u2類型的數據的集合, Class文件中由這三項數據來確定該類型的繼承關系。 類索引用於確定這個類的全限定名, 父類索引用於確定這個類的父類的全限定名。 由於Java語言不允許多重繼承, 所以父類索引只有一個, 除了java.lang.Object之外, 所有的Java類都有父類, 因此除了java.lang.Object外, 所有Java類的父類索引都不為0。 接口索引集合就用來描述這個類實現了哪些接口, 這些被實現的接口將按implements關鍵字(如果這個Class文件表示的是一個接口, 則應當是extends關鍵字) 后的接口順序從左到右排列在接口索引集合中。
2.5、字段表集合
字段表(field_info) 用於描述接口或者類中聲明的變量。 Java語言中的“字段”(Field) 包括類級變量以及實例級變量, 但不包括在方法內部聲明的局部變量。 字段可以包括的修飾符有字段的作用域(public、 private、 protected修飾符) 、 是實例變量還是類變量(static修飾符) 、 可變性(final) 、 並發可見性(volatile修飾符, 是否強制從主內存讀寫) 、 可否被序列化(transient修飾符) 、 字段數據類型(基本類型、 對象、 數組) 、字段名稱。 上述這些信息中, 各個修飾符都是布爾值, 要么有某個修飾符, 要么沒有, 很適合使用標志位來表示。 而字段叫做什么名字、 字段被定義為什么數據類型, 這些都是無法固定的, 只能引用常量池中的常量來描述。 字段表的最終格式如下。
| 類型 | 名稱 | 數量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
而字段修飾符放在access_flags項目中, 它與類中的access_flags項目是非常類似的, 都是一個u2的數據類型, 其中可以設置的標志位和含義如下所示:
| 標志名稱 | 標志值 | 含義 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 字段是否為public |
| ACC_PRIVATE | 0x0002 | 字段是否為private |
| ACC_PROTECTED | 0x0004 | 字段是否為protected |
| ACC_STATIC | 0x0008 | 字段是否為static |
| ACC_FINAL | 0x0010 | 字段是否為final |
| ACC_SYNCHRONIZED | 0x0020 | 字段是否為synchronized |
| ACC_TRANSIENT | 0x0080 | 字段是否為transient |
| ACC_ABSTRACT | 0x0400 | 字段是否為abstract |
| ACC_SYNTHETIC | 0x1000 | 字段是否為編譯器自動產生 |
name_index和descriptor_index。 它們都是對常量池項的引用, 分別代表着字段的簡單名稱以及字段和方法的描述符。
全限定名:僅僅是把類全名中的“.”替換成了“/”而已,例如類名org.apache.xxxx,器全限定名為org/apache/xxxx。
簡單名稱:就是指沒有類型和參數修飾的方法或者字段名稱, 比如類中的inc()方法和m字段的簡單名稱分別就是“inc”和“m”。
方法和字段的描述符:描述符的作用是用來描述字段的數據類型、 方法的參數列表(包括數量、 類型以及順序) 和返回值。 根據描述符規則, 基本數據類型(byte、 char、 double、 float、 int、 long、 short、 boolean) 以及代表無返回值的void類型都用一個大寫字符來表示, 而對象類型則用字符L加對象的全限定名來表示,祥見下表:
| 標識字符 | 含義 |
| B | 基本類型byte |
| C | 基本類型char |
| D | 基本類型double |
| F | 基本類型float |
| I | 基本類型int |
| J | 基本類型long |
| S | 基本類型short |
| Z | 基本類型boolean |
| V | 特殊類型void |
| L | 對象類型,如java/lang/Object |
對於數組類型, 每一維度將使用一個前置的“[”字符來描述, 如一個定義為“java.lang.String[][]”類型的二維數組將被記錄成“[[Ljava/lang/String; ”, 一個整型數組“int[]”將被記錄成“[I”
用描述符來描述方法時, 按照先參數列表、 后返回值的順序描述, 參數列表按照參數的嚴格順序放在一組小括號“()”之內。 如方法void inc()的描述符為“()V”, 方法java.lang.String toString()的描述符為“()Ljava/lang/String; ”, 方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target,int targetOffset, int targetCount, int fromIndex)的描述符為“([CII[CIII)I”
2.6、方法表集合
Class文件存儲格式中對方法的描述與對字段的描述采用了幾乎完全一致的方式, 方法表的結構如同字段表一樣, 依次包括訪問標志(access_flags) 、 名稱索引(name_index) 、 描述符索引(descriptor_index) 、 屬性表集合(attributes) 幾項,如下圖所示

在訪問標志和屬性表集合的可選項中有所區別,因為volatile關鍵字和transient關鍵字不能修飾方法, 所以方法表的訪問標志中沒有了ACC_VOLATILE標志和ACC_TRANSIENT標志。 與之相對, synchronized、 native、 strictfp和abstract關鍵字可以修飾方法, 方法表的訪問標志中也相應地增加了ACC_SYNCHRONIZED、ACC_NATIVE、 ACC_STRICTFP和ACC_ABSTRACT標志。
2.7、屬性表集合
1、code屬性
方法的定義可以通過訪問標志、 名稱索引、 描述符索引來表達清楚, 但方法里面的代碼去哪里了? 方法里的Java代碼, 經過Javac編譯器編譯成字節碼指令之后, 存放在方法屬性表集合中一個名為“Code”的屬性里面, 屬性表作為Class文件格式中最具擴展性的一種數據項目,
java程序方法體里面的代碼經過Javac編譯器處理之后, 最終變為字節碼指令存儲在Code屬性內。Code屬性出現在方法表的屬性集合之中, 但並非所有的方法表都必須存在這個屬性, 譬如接口或者抽象類中的方法就不存在Code屬性。
Code屬性是Class文件中最重要的一個屬性, 如果把一個Java程序中的信息分為代碼(Code, 方法體里面的Java代碼) 和元數據(Metadata, 包括類、 字段、 方法定義及其他信息) 兩部分, 那么在整個Class文件里, Code屬性用於描述代碼, 所有的其他數據項目都用於描述元數據。
2、Exceptions屬性
Exceptions屬性的作用是列舉出方法中可能拋出的受查異常(Checked Excepitons) , 也就是方法描述時在throws關鍵字后面列舉的異常。
3、LineNumberTable屬性
LineNumberTable屬性用於描述Java源碼行號與字節碼行號(字節碼的偏移量) 之間的對應關系。並不是運行時必需的屬性, 但默認會生成到Class文件之中, 可以在Javac中使用-g: none或-g: lines選項來取消或要求生成這項信息。
4、LocalVariableTable及LocalVariableTypeTable屬性
LocalVariableTable屬性用於描述棧幀中局部變量表的變量與Java源碼中定義的變量之間的關系, 它也不是運行時必需的屬性, 但默認會生成到Class文件之中, 可以在Javac中使用-g: none或-g: vars選項來取消或要求生成這項信息
5、SourceFile及SourceDebugExtension屬性
SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱。 這個屬性也是可選的, 可以使用Javac的-g: none或-g: source選項來關閉或要求生成這項信息。 在Java中, 對於大多數的類來說, 類名和文件名是一致的, 但是有一些特殊情況(如內部類) 例外
SourceDebugExtension屬性用於存儲額外的代碼調試信息。 典型的場景是在進行JSP文件調試時, 無法通過Java堆棧來定位到JSP文件的行號。
6、ConstantValue屬性
ConstantValue屬性的作用是通知虛擬機自動為靜態變量賦值。 只有被static關鍵字修飾的變量(類變量) 才可以使用這項屬性。 類似“int x=123”和“static int x=123”這樣的變量定義在Java程序里面是非常常見的事情, 但虛擬機對這兩種變量賦值的方式和時刻都有所不同。 對非static類型的變量(也就是實例變量) 的賦值是在實例構造器<init>()方法中進行的; 而對於類變量, 則有兩種方式可以選擇: 在類構造器<clinit>()方法中或者使用ConstantValue屬性。
7、InnerClasses屬性
InnerClasses屬性用於記錄內部類與宿主類之間的關聯。 如果一個類中定義了內部類, 那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性
8、Deprecated及Synthetic屬性
Deprecated和Synthetic兩個屬性都屬於標志類型的布爾屬性, 只存在有和沒有的區別, 沒有屬性值的概念。
Deprecated屬性用於表示某個類、 字段或者方法, 已經被程序作者定為不再推薦使用, 它可以通過代碼中使用“@deprecated”注解進行設置
Synthetic屬性代表此字段或者方法並不是由Java源碼直接產生的, 而是由編譯器自行添加的, 在JDK 5之后, 標識一個類、 字段或者方法是編譯器自動產生的, 也可以設置它們訪問標志中的ACC_SYNTHETIC標志位。
9、StackMapTable屬性
StackMapTable是一個相當復雜的變長屬性, 位於Code屬性的屬性表中。 這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(TypeChecker), 目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器。
StackMapTable屬性中包含零至多個棧映射幀(Stack Map Frame) , 每個棧映射幀都顯式或隱式地代表了一個字節碼偏移量, 用於表示執行到該字節碼時局部變量表和操作數棧的驗證類型。 類型檢查驗證器會通過檢查目標方法的局部變量和操作數棧所需要的類型來確定一段字節碼指令是否符合邏輯約束。
10、Signature屬性
Signature屬性是一個可選的定長屬性, 可以出現於類、 字段表和方法表結構的屬性表中。 任何類、 接口、 初始化方法或成員的泛型簽名如果包含了類型變量(Type Variable) 或參數化類型(ParameterizedType) , 則Signature屬性會為它記錄泛型簽名信息。 之所以要專門使用這樣一個屬性去記錄泛型類型, 是因為Java語言的泛型采用的是擦除法實現的偽泛型, 字節碼(Code屬性) 中所有的泛型信息編譯(類型變量、 參數化類型) 在編譯之后都通通被擦除掉。
11、BootstrapMethods屬性
BootstrapMethods是一個復雜的變長屬性, 位於類文件的屬性表中。 這個屬性用於保存invokedynamic指令引用的引導方法限定符。
12、MethodParameters屬性
MethodParameters是一個用在方法表中的變長屬性。MethodParameters的作用是記錄方法的各個形參名稱和信息。
13、模塊化相關屬性
JDK 9的一個重量級功能是Java的模塊化功能, 因為模塊描述文件(module-info.java) 最終是要編譯成一個獨立的Class文件來存儲的, 所以, Class文件格式也擴展了Module、 ModulePackages和ModuleMainClass三個屬性用於支持Java模塊化相關功能。
Module屬性是一個非常復雜的變長屬性, 除了表示該模塊的名稱、 版本、 標志信息以外, 還存儲了這個模塊requires、 exports、 opens、 uses和provides定義的全部內容,
ModulePackages是另一個用於支持Java模塊化的變長屬性, 它用於描述該模塊中所有的包, 不論是不是被export或者open的。
ModuleMainClass屬性是一個定長屬性, 用於確定該模塊的主類(Main Class)
參考:
《深入理解java虛擬機第三版》
