Class類文件結構

平臺無關性

  • 如圖,源碼經過編譯得到的字節碼文件可以由運行Java虛擬機的機器運行,因此可以說字節碼是Java語言跨平臺的基石,同樣也是其他語言跨平臺的有效途徑

  • 只要目標語言的編譯器按照Java字節碼存儲規範進行編譯,那麼得到的class文件都可以被正確執行

Class文件結構

  • Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部都是程序運行的必要數據。根據Java虛擬機規範的規定,Class文件格式採用一種類似於C語言結構體的僞結構來存儲,這種僞結構中只有兩種數據類型:無符號數和表

  • 無符號數屬於基本數據類型,主要可以用來描述數字、索引符號、數量值或者按照UTF-8編碼構成的字符串值,大小使用u1、u2、u4、u8分別表示1字節、2字節、4字節和8字節

  • 表是由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有的表都習慣以“_info”結尾。那麼表是幹嘛的呢?表主要用於描述有層次關係的複合結構的數據,比如方法、字段。需要注意的是class文件是沒有分隔符的,所以每個的二進制數據類型都是嚴格定義的

  • 整個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 attribute_count(附加屬性的個數) 1
attribute_info attributes(附加屬性的表) attributes_count
  • 從表中可以看出,無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的該數據項的形式,稱這一系列連續的摸一個類型的數據爲某一類型的集合,比如,fields_count個field_info表數據構成了字段表集合。這裏需要說明的是:Class文件中的數據項,都是嚴格按照上表中的順序和數量被嚴格限定的,每個字節代表的含義,長度,先後順序等都不允許改變

  • 在class文件中,主要分爲魔數、Class文件的版本號、常量池、訪問標誌、類索引(還包括父類索引和接口索引集合)、字段表集合、方法表集合、屬性表集合。下面就分別對每一種文件進行說明

魔數與Class文件版本號

  • 每個class字節碼文件頭4個字節爲魔數,作用是用於確定該文件是否能被虛擬機接受,固定爲0xCAFEBABE

  • 第5和第6個字節是次版本號,第7和第8是主版本號。因此,字節碼文件的版本號確定了能執行該程序的虛擬機JDK版本。

  • 用Sublime text打開自己的一個class文件,主版本號是50,因此要求JDK至少是1.6以上版本

cafe babe 0000 0032 00f1 0a00 3d00 7707
0078 0800 790a 0002 007a 0700 7b0a 0005
0077 0800 7c0a 0005 007d 0800 7e0a 0005
007f 0a00 8000 810a 001a 0082 0a00 1a00
830a 001a 0084 0700 850a 000f 0077 0800
860b 0087 0088 0700 890b 008a 008b 0800
8c08 008d 0800 8e07 008f 0800 9007 0091
0700 9207 0093 0700 940a 001d 007a 0a00
1c00 950a 001b 0096 0a00 1b00 9708 0098
0a00 9900 9a08 009b 0800 9c0a 001a 009d

常量池

  • 常量池可以簡單理解爲class文件的資源從庫,這種數據類型是Class文件結構中與其他項目關聯最多的數據類型,也是佔用Class文件空間最大的項目之一

  • 常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近於Java層面的常量概念,如文本字符串、被聲明爲final的常量值等。而符號引用總結起來則包括了下面三類常量:

  • 類和接口的全限定名(即帶有包名的Class名,如:org.lxh.test.TestClass)

  • 字段的名稱和描述符(private、static等描述符)

  • 方法的名稱和描述符(private、static等描述符)

虛擬機在加載Class文件時纔會進行動態連接,也就是說,Class文件中不會保存各個方法和字段的最終內存佈局信息,因此,這些字段和方法的符號引用不經過轉換是無法直接被虛擬機使用的。當虛擬機運行時,需要從常量池中獲得對應的符號引用,再在類加載過程中的解析階段將其替換爲直接引用,並翻譯到具體的內存地址中

符號引用和直接引用的區別與關聯

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到了內存中

  • 直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在於內存之中了

在常量池中每一項常量都是一個表,在jdk1.7中共有14中常量類型,所以常量池的項目就對應14張表,這14張表的每種類型都不一樣。但是有一個共同特點:表開始的第一位都是一個u1類型的標誌位,代表這個常量屬於哪種類型

類型 標記 描述
CONSTANT_Utf8_info 1 UTF-8編碼的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 5 長整形字面量
CONSTANT_Double_info 6 雙精度字面量
CONSTANT_Class_info 7 類&接口符號引用
CONSTANT_String_info 8 字符串類型字面量
CONSTANT_Fieldref_info 9 字段符號引用
CONSTANT_Methodref_info 10 類中方法符號引用
CONSTANT_InterfaceMethodref_info 11 接口方法符號引用
CONSTANT_NameAndType_info 12 字段&方法部分符號引用
  • 需要注意的是,在Class文件中,方法、字段都需要引用CONSTANT-Utf8_info類型的常量,所以這種類型的常量的長度有一定的限制,也就是Java中方法、字段的最大長度。在CONSTANT-Utf8_info中,其length的值u2,說明Java虛擬機只能編譯最大大約64KB的變量或者方法名。超過的話將不會進行編譯

訪問標誌

  • 常量池之後的數據結構是訪問標誌(access_flags),這個標誌主要用於識別一些類或者接口層次的訪問信息,主要包括:這個Class是類還是接口、是否定義public、是否定義abstract類型;如果是類的話是否被聲明爲final等。具體的標誌訪問如下:

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

  • 這個數據項主要用於確定這個類的繼承關係。其中類索引和父類所以都是一個u2類型的數據,而接口索引集合是一組u2類型的數據。在Java中由於不允許多繼承,所以父類索引是唯一的,但是一個類可以實現多個接口,所以得到的接口索引是一個集合,表示這個類實現了哪些接口

字段表集合

字段表用於描述接口或者類中聲明的變量。字段包括類級變量和實例級變量,但是不包括方法內部聲明的局部變量(這些變量是存儲在Java虛擬機棧中的局部變量表中的)。自然,描述一個字段的信息包括:字段的作用域(public、protected、private)、實例變量與否(static)、可變性(final)、併發可見性(volatile)、可否被序列化(transient)、字段數據類型(基本數據類型、對象、數組)、字段名稱。字段的信息也被存放在一張表中,其字段表包括三種類型:

  • u2類型訪問標誌(access_flags)

  • u2類型的name_index(字段的簡單名稱)

  • u2類型的描述符(descriptor_index)

其訪問標誌在access_flags中,如下圖所示:

  • 上面出現了簡單名稱,上文中出現了全限定名,以及這裏出現的描述符,三者有什麼區別呢?其中全限定名稱比較好理解,就是類的完整路徑信息。而簡單名稱則是指沒有類型和參數修飾的方法或者字段名稱,比如一個方法如下:
public void inc(int a,int b){
    System.out.println(a+b);
}

那麼這個方法的簡單名稱就是inc。

  • 相對於以上兩者,描述符相對複雜一些。描述符的主要的作用是描述字段的數據類型、方法的參數列表和返回值。其中我們熟悉的void,在Class文件中用V表示。下面是完整的描述符標誌的含義:

  • 對於數組類型,每一維度使用一個前置的“[”字符描述,如果是二維數組,那麼就有兩個“[”符號。比如“java.lang.String[][]”會被記錄成“[[Ljava.lang.String;”

  • 對於方法,則是按照縣參數列表後返回值的順序進行描述的。比如方法int inc(int a,int[] b,char[][] c,int d)的描述符是“(I[I[[CI)I”

方法表集合

  • JVM中堆方法表的描述與字段表是一致的,包括了:訪問標誌、名稱索引、描述符索引、屬性表集合。方法表單額結構與字段表是一致的,區別在於訪問標誌的不同.在方法中不能了用volatile和transient關鍵字修飾,所以這兩個標誌不能用在方法表中。在方法中添加了字段不能使用的訪問標誌,比如方法可以使用synchronized、native、strictfp、abstract關鍵字修飾,所以在方法表中就增加了相應的訪問標誌

  • 要注意的是,如果父類方法沒有在子類中重寫,那麼在方法中不會自動出現來自父類的方法信息。同樣的,有可能添加編譯器自動增加的方法,比如方法

屬性表集合

  • 前面的Class文件、字段表和方法表都可以攜帶自己的屬性信息,這個信息用屬性表進行描述,用於描述某些場景專有的信息。在屬性表中沒有類似Class文件的數據項目類型和順序的嚴格要求,只要新的屬性不與現有的屬性名重複,任何人都可以向屬性表中寫入自己定義的屬性信息

Code屬性

  • Java程序方法體中的代碼經過javac編譯最終編譯成的字節碼指令就保存在Code屬性中。但是並非所有的方法表都必須存在這個屬性。Code屬性是Class文件中最重要的一個屬性,如果把一個Java程序中的信息分爲代碼(Code)和元數據(Metadata,包括類、字段、方法定義及其其他信息)兩部分,那麼在整個Class文件中,Code屬性用於描述代碼,所有其他的數據項目都用於描述元數據

Exceptions屬性

  • 這個屬性的作用是列舉出方法中可能拋出的受查異常(Checked Exception),也就是描述throws 後的列舉的異常

LineNumberTable屬性

  • 主要用於描述Java源代碼行號與字節碼行號之間的對應關係。這個屬性也不是必須的。如果沒有這個屬性,對程序的直接影響就是當拋出異常的時候無法顯示對應的行號;並且在調試的時候無法通過設置斷點的方法是調試程序

LocalVariableTable屬性

  • 用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量的之間的關係。也不屬於必須的屬性。如果沒有這個屬性,產生的直接影響就是當別人引用這個方法的時候,所有的參數名稱都會丟失,IDE將會使用諸如args0、args1之類的參數進行顯示。自然,當調試程序的時候,顯示的參數名稱是不可知的

SourceFile屬性

  • 用於記錄這個Class文件的源碼文件名稱。如果不使用這個屬性,那麼當拋出異常的時候,堆棧中將不會顯示出錯代碼所屬的文件名

ConstantValue屬性

作用是通知虛擬機自動爲靜態變量賦值。要注意的是,只有被static關鍵字修飾的額變量纔可以使用這個屬性(類變量)。對於非類變量,初始化是在方法中進行的;對於類變量可以選擇兩種方式進行變量的初始化:一是在類構造器方法中使用;二是是ConstantValue屬性。目前Sun Hotspot的選擇原則是:如果一個變量同時使用static和final關鍵字修飾,並且這個變量是基本數據類型或者java.lang.String類型的話,就使用ConstantValue屬性進行初始化。如果沒有被final修飾或者並非是基本數據類型,那麼將會選擇使用方法進行初始化

InnerClass屬性

  • 這個屬性主要用於記錄內部類與宿主類之間的關聯關係

Deprecated以及Synthetic屬性

  • 這兩個屬性都屬於標誌類型的布爾屬性,只存在有沒有的區別。

  • Deprecated屬性用於表示某個類、字段或者方法,已經被程序作者定爲不再推薦使用,可以通過註解@deprecated實現

  • Synthetic屬性代表此字段並不是由Java源碼產生的,而是通過編譯器自行添加的

StackMapTable屬性

  • 該屬性的目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器

Signature屬性

  • 這個屬性是專門用來記錄泛型類型的,因爲在Java語言採用的是擦除法實現的泛型,在字節碼(Code屬性)中,泛型信息編譯之後會被擦除。擦除法的優點是能夠節省泛型所佔的內存空間,缺點是在運行期間無法通過反射得到泛型信息,而Signature屬性則彌補了這一缺陷。現在的Java反射API已經能夠得到泛型信息,功勞就在於這個屬性

BootstrapMethods屬性

  • 這個屬性用於保存invokedynamic指令引用的引導方法限定符。(該指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法)
發佈了67 篇原創文章 · 獲贊 26 · 訪問量 73萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章