java class文件詳解
一、概述
各種不同平台的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 (下載地址://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虛擬機第三版》