各類不一樣平臺的Java虛擬機, 以及全部平臺都統一支持的程序存儲格式——字節碼(Byte Code)是構成平臺無關性的基石,因此class文件主要用於解決平臺無關性的中間文件。以下圖所示:html
java虛擬機不與包括Java語言在內的任何程序語言綁定, 它只與「Class文件」這種特定的二進制文件格式所關聯, Class文件中包含了Java虛擬機指令集、 符號表以及若干其餘輔助信息。java
每個class文件都對應着惟一一個類或者接口的定義信息,可是相對地,類或者接口並不必定都必須定義在文件裏(好比類或者接口也能夠經過類加載器直接生成)apache
每一個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語言的結構體來存儲數據的。bootstrap
Class文件格式採用一種相似於C語言結構體的僞結構來存儲數據,主要有兩類數據項,無符號數和表,無符號數用來表述數字,索引引用以及字符串等,好比 u1,u2,u4,u8分別表明1個字節,2個字節,4個字節,8個字節的無符號數,而表是任意數量的可變長項組成,是有多個無符號數以及其它的表組成的複合結構,全部表的命名都習慣性地以「_info」結尾,不管是無符號數仍是表, 當須要描述同一類型但數量不定的多個數據時, 常常會使用一個前置的容量計數器加若干個連續的數據項的形式, 這時候稱這一系列連續的某一類型的數據爲某一類型的「集合」。 數組
類型 | 名稱 | 數量 |
---|---|---|
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 |
每一個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)工具打開,獲得以下的圖,前面幾位就是魔數和版本號
ide
這裏能夠得出咱們使用的版本爲java1.8,16進制的34等於10進制的52模塊化
緊接着主、 次版本號以後的是常量池入口, 常量池能夠比喻爲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的常量值等。
符號引用則屬於編譯原理方面的概念, 主要包括下面幾類常量:
虛擬機在加載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表示包名 |
在常量池結束以後,緊接着的兩個字節表明訪問標誌(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 |
標識這是一個模塊 |
類索引(this_class) 和父類索引(super_class) 都是一個u2類型的數據, 而接口索引集合(interfaces) 是一組u2類型的數據的集合, Class文件中由這三項數據來肯定該類型的繼承關係。 類索引用於肯定這個類的全限定名, 父類索引用於肯定這個類的父類的全限定名。 因爲Java語言不容許多重繼承, 因此父類索引只有一個, 除了java.lang.Object以外, 全部的Java類都有父類, 所以除了java.lang.Object外, 全部Java類的父類索引都不爲0。 接口索引集合就用來描述這個類實現了哪些接口, 這些被實現的接口將按implements關鍵字(若是這個Class文件表示的是一個接口, 則應當是extends關鍵字) 後的接口順序從左到右排列在接口索引集合中。
字段表(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」
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標誌。
一、code屬性
方法的定義能夠經過訪問標誌、 名稱索引、 描述符索引來表達清楚, 但方法裏面的代碼去哪裏了? 方法裏的Java代碼, 通過Javac編譯器編譯成字節碼指令以後, 存放在方法屬性表集合中一個名爲「Code」的屬性裏面, 屬性表做爲Class文件格式中最具擴展性的一種數據項目,
java程序方法體裏面的代碼通過Javac編譯器處理以後, 最終變爲字節碼指令存儲在Code屬性內。Code屬性出如今方法表的屬性集合之中, 但並不是全部的方法表都必須存在這個屬性, 譬如接口或者抽象類中的方法就不存在Code屬性。
Code屬性是Class文件中最重要的一個屬性, 若是把一個Java程序中的信息分爲代碼(Code, 方法體裏面的Java代碼) 和元數據(Metadata, 包括類、 字段、 方法定義及其餘信息) 兩部分, 那麼在整個Class文件裏, Code屬性用於描述代碼, 全部的其餘數據項目都用於描述元數據。
二、Exceptions屬性
Exceptions屬性的做用是列舉出方法中可能拋出的受查異常(Checked Excepitons) , 也就是方法描述時在throws關鍵字後面列舉的異常。
三、LineNumberTable屬性
LineNumberTable屬性用於描述Java源碼行號與字節碼行號(字節碼的偏移量) 之間的對應關係。並非運行時必需的屬性, 但默認會生成到Class文件之中, 能夠在Javac中使用-g: none或-g: lines選項來取消或要求生成這項信息。
四、LocalVariableTable及LocalVariableTypeTable屬性
LocalVariableTable屬性用於描述棧幀中局部變量表的變量與Java源碼中定義的變量之間的關係, 它也不是運行時必需的屬性, 但默認會生成到Class文件之中, 能夠在Javac中使用-g: none或-g: vars選項來取消或要求生成這項信息
五、SourceFile及SourceDebugExtension屬性
SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱。 這個屬性也是可選的, 可使用Javac的-g: none或-g: source選項來關閉或要求生成這項信息。 在Java中, 對於大多數的類來講, 類名和文件名是一致的, 可是有一些特殊狀況(如內部類) 例外
SourceDebugExtension屬性用於存儲額外的代碼調試信息。 典型的場景是在進行JSP文件調試時, 沒法經過Java堆棧來定位到JSP文件的行號。
六、ConstantValue屬性
ConstantValue屬性的做用是通知虛擬機自動爲靜態變量賦值。 只有被static關鍵字修飾的變量(類變量) 纔可使用這項屬性。 相似「int x=123」和「static int x=123」這樣的變量定義在Java程序裏面是很是常見的事情, 但虛擬機對這兩種變量賦值的方式和時刻都有所不一樣。 對非static類型的變量(也就是實例變量) 的賦值是在實例構造器<init>()方法中進行的; 而對於類變量, 則有兩種方式能夠選擇: 在類構造器<clinit>()方法中或者使用ConstantValue屬性。
七、InnerClasses屬性
InnerClasses屬性用於記錄內部類與宿主類之間的關聯。 若是一個類中定義了內部類, 那編譯器將會爲它以及它所包含的內部類生成InnerClasses屬性
八、Deprecated及Synthetic屬性
Deprecated和Synthetic兩個屬性都屬於標誌類型的布爾屬性, 只存在有和沒有的區別, 沒有屬性值的概念。
Deprecated屬性用於表示某個類、 字段或者方法, 已經被程序做者定爲再也不推薦使用, 它能夠經過代碼中使用「@deprecated」註解進行設置
Synthetic屬性表明此字段或者方法並非由Java源碼直接產生的, 而是由編譯器自行添加的, 在JDK 5以後, 標識一個類、 字段或者方法是編譯器自動產生的, 也能夠設置它們訪問標誌中的ACC_SYNTHETIC標誌位。
九、StackMapTable屬性
StackMapTable是一個至關複雜的變長屬性, 位於Code屬性的屬性表中。 這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(TypeChecker), 目的在於代替之前比較消耗性能的基於數據流分析的類型推導驗證器。
StackMapTable屬性中包含零至多個棧映射幀(Stack Map Frame) , 每一個棧映射幀都顯式或隱式地表明瞭一個字節碼偏移量, 用於表示執行到該字節碼時局部變量表和操做數棧的驗證類型。 類型檢查驗證器會經過檢查目標方法的局部變量和操做數棧所須要的類型來肯定一段字節碼指令是否符合邏輯約束。
十、Signature屬性
Signature屬性是一個可選的定長屬性, 能夠出現於類、 字段表和方法表結構的屬性表中。 任何類、 接口、 初始化方法或成員的泛型簽名若是包含了類型變量(Type Variable) 或參數化類型(ParameterizedType) , 則Signature屬性會爲它記錄泛型簽名信息。 之因此要專門使用這樣一個屬性去記錄泛型類型, 是由於Java語言的泛型採用的是擦除法實現的僞泛型, 字節碼(Code屬性) 中全部的泛型信息編譯(類型變量、 參數化類型) 在編譯以後都統統被擦除掉。
十一、BootstrapMethods屬性
BootstrapMethods是一個複雜的變長屬性, 位於類文件的屬性表中。 這個屬性用於保存invokedynamic指令引用的引導方法限定符。
十二、MethodParameters屬性
MethodParameters是一個用在方法表中的變長屬性。MethodParameters的做用是記錄方法的各個形參名稱和信息。
1三、模塊化相關屬性
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虛擬機第三版》