例項分析JAVA CLASS的檔案結構

Java__moon發表於2019-02-21

今天把之前在Evernote中的筆記重新整理了一下,發上來供對java class 檔案結構的有興趣的同學參考一下。

學習Java的朋友應該都知道Java從剛開始的時候就打著平臺無關性的旗號,說“一次編寫,到處執行”,其實說到無關性,Java平臺還有另外一個無關 性那就是語言無關性,要實現語言無關性,那麼Java體系中的class的檔案結構或者說是位元組碼就顯得相當重要了,其實Java從剛開始的時候就有兩套 規範,一個是Java語言規範,另外一個是Java虛擬機器規範,Java語言規範只是規定了Java語言相關的約束以及規則,而虛擬機器規範則才是真正從跨 平臺的角度去設計的。今天我們就以一個實際的例子來看看,到底Java中一個Class檔案對應的位元組碼應該是什麼樣子。 這篇文章將首先總體上闡述一下Class到底由哪些內容構成,然後再用一個實際的Java類入手去分析class的檔案結構。

在繼續之前,我們首先需要明確如下幾點:

1)Class檔案是有8個位元組為基礎的位元組流構成的,這些位元組流之間都嚴格按照規定的順序排列,並且位元組之間不存在任何空隙,對於超過8個位元組的資料,將按 照Big-Endian的順序儲存的,也就是說高位位元組儲存在低的地址上面,而低位位元組儲存到高地址上面,其實這也是class檔案要跨平臺的關鍵,因為 PowerPC架構的處理採用Big-Endian的儲存順序,而x86系列的處理器則採用Little-Endian的儲存順序,因此為了Class文 件在各中處理器架構下保持統一的儲存順序,虛擬機器規範必須對起進行統一。

2) Class檔案結構採用類似C語言的結構體來儲存資料的,主要有兩類資料項,無符號數和表,無符號數用來表述數字,索引引用以及字串等,比如 u1,u2,u4,u8分別代表1個位元組,2個位元組,4個位元組,8個位元組的無符號數,而表是有多個無符號數以及其它的表組成的複合結構。可能大家看到這裡 對無符號數和表到底是上面也不是很清楚,不過不要緊,等下面例項的時候,我會再以例項來解釋。

明確了上面的兩點以後,我們接下來後來看看Class檔案中按照嚴格的順序排列的位元組流都具體包含些什麼資料:

 

(上圖來自The Java Virtual Machine Specification Java SE 7 Edition)

在看上圖的時候,有一點我們需要注意,比如cp_info,cp_info表示常量池,上圖中用 constant_pool[constant_pool_count-1]的方式來表示常量池有constant_pool_count-1個常量,它 這裡是採用陣列的表現形式,但是大家不要誤以為所有的常量池的常量長度都是一樣的,其實這個地方只是為了方便描述採用了陣列的方式,但是這裡並不像程式設計語 言那裡,一個int型的陣列,每個int長度都一樣。明確了這一點以後,我們在回過頭來看看上圖中每一項都具體代表了什麼含義。

1)u4 magic 表示魔數,並且魔數佔用了4個位元組,魔數到底是做什麼的呢?它其實就是表示一下這個檔案的型別是一個Class檔案,而不是一張JPG圖片,或者AVI的電影。而Class檔案對應的魔數是0xCAFEBABE.

2)u2 minor_version 表示Class檔案的次版本號,並且此版本號是u2型別的無符號數表示。

3) u2 major_version 表示Class檔案的主版本號,並且主版本號是u2型別的無符號數表示。major_version和minor_version主要用來表示當前的虛擬 機是否接受當前這種版本的Class檔案。不同版本的Java編譯器編譯的Class檔案對應的版本是不一樣的。高版本的虛擬機器支援低版本的編譯器編譯的 Class檔案結構。比如Java SE 6.0對應的虛擬機器支援Java SE 5.0的編譯器編譯的Class檔案結構,反之則不行。

4) u2 constant_pool_count 表示常量池的數量。這裡我們需要重點來說一下常量池是什麼東西,請大家不要與Jvm記憶體模型中的執行時常量池混淆了,Class檔案中常量池主要儲存了字 面量以及符號引用,其中字面量主要包括字串,final常量的值或者某個屬性的初始值等等,而符號引用主要儲存類和介面的全限定名稱,欄位的名稱以及描 述符,方法的名稱以及描述符,這裡名稱可能大家都容易理解,至於描述符的概念,放到下面說欄位表以及方法表的時候再說。另外大家都知道Jvm的記憶體模型中 有堆,棧,方法區,程式計數器構成,而方法區中又存在一塊區域叫執行時常量池,執行時常量池中存放的東西其實也就是編譯器長生的各種字面量以及符號引用, 只不過執行時常量池具有動態性,它可以在執行的時候向其中增加其它的常量進去,最具代表性的就是String的intern方法。

5)cp_info 表示常量池,這裡面就存在了上面說的各種各樣的字面量和符號引用。放到常量池的中資料項在The Java Virtual Machine Specification Java SE 7 Edition 中一共有14個常量,每一種常量都是一個表,並且每種常量都用一個公共的部分tag來表示是哪種型別的常量。

下面分別簡單描述一下具體細節等到後面的例項 中我們再細化。

  • CONSTANT_Utf8_info      tag標誌位為1,   UTF-8編碼的字串
  • CONSTANT_Integer_info  tag標誌位為3, 整形字面量
  • CONSTANT_Float_info     tag標誌位為4, 浮點型字面量
  • CONSTANT_Long_info     tag標誌位為5, 長整形字面量
  • CONSTANT_Double_info  tag標誌位為6, 雙精度字面量
  • CONSTANT_Class_info    tag標誌位為7, 類或介面的符號引用
  • CONSTANT_String_info    tag標誌位為8,字串型別的字面量
  • CONSTANT_Fieldref_info  tag標誌位為9,  欄位的符號引用
  • CONSTANT_Methodref_info  tag標誌位為10,類中方法的符號引用
  • CONSTANT_InterfaceMethodref_info tag標誌位為11, 介面中方法的符號引用
  • CONSTANT_NameAndType_info tag 標誌位為12,欄位和方法的名稱以及型別的符號引用

6) u2 access_flags 表示類或者介面的訪問資訊,具體如下圖所示:

7)u2 this_class 表示類的常量池索引,指向常量池中CONSTANT_Class_info的常量

8)u2 super_class 表示超類的索引,指向常量池中CONSTANT_Class_info的常量

9)u2 interface_counts 表示介面的數量

10)u2 interface[interface_counts]表示介面表,它裡面每一項都指向常量池中CONSTANT_Class_info常量

11)u2 fields_count 表示類的例項變數和類變數的數量

12) field_info fields[fields_count]表示欄位表的資訊,其中欄位表的結構如下圖所示:

上圖中access_flags表示欄位的訪問表示,比如欄位是public,private,protect 等,name_index表示欄位名 稱,指向常量池中型別是CONSTANT_UTF8_info的常量,descriptor_index表示欄位的描述符,它也指向常量池中型別為 CONSTANT_UTF8_info的常量,attributes_count表示欄位表中的屬性表的數量,而屬性表是則是一種用與描述欄位,方法以及 類的屬性的可擴充套件的結構,不同版本的Java虛擬機器所支援的屬性表的數量是不同的。

13) u2 methods_count表示方法表的數量

14)method_info 表示方法表,方法表的具體結構如下圖所示:


其中access_flags表示方法的訪問表示,name_index表示名稱的索引,descriptor_index表示方法的描述 符,attributes_count以及attribute_info類似欄位表中的屬性表,只不過欄位表和方法表中屬性表中的屬性是不同的,比如方法 表中就Code屬性,表示方法的程式碼,而欄位表中就沒有Code屬性。其中具體Class中到底有多少種屬性,等到Class檔案結構中的屬性表的時候再 說說。

15) attribute_count表示屬性表的數量,說到屬性表,我們需要明確以下幾點:

  • 屬性表存在於Class檔案結構的最後,欄位表,方法表以及Code屬性中,也就是說屬性表中也可以存在屬性表
  • 屬性表的長度是不固定的,不同的屬性,屬性表的長度是不同的

上面說完了Class檔案結構中每一項的構成以後,我們以一個實際的例子來解釋以下上面所說的內容。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

package com.ejushang.TestClass;

 

public class TestClass implements Super{

 

private static final int staticVar = 0;

 

private int instanceVar=0;

 

public int instanceMethod(int param){

 return param+1;

 }

 

}

 

interface Super{ }

通過jdk1.6.0_37的javac 編譯後的TestClass.java對應的TestClass.class的二進位制結構如下圖所示:

下面我們就根據前面所說的Class的檔案結構來解析以下上圖中位元組流。

1)魔數
從Class的檔案結構我們知道,剛開始的4個位元組是魔數,上圖中從地址00000000h-00000003h的內容就是魔數,從上圖可知Class的檔案的魔數是0xCAFEBABE。

2)主次版本號
接下來的4個位元組是主次版本號,有上圖可知從00000004h-00000005h對應的是0x0000,因此Class的minor_version 為0x0000,從00000006h-00000007h對應的內容為0x0032,因此Class檔案的major_version版本為 0x0032,這正好就是jdk1.6.0不帶target引數編譯後的Class對應的主次版本。

3)常量池的數量
接下來的2個位元組從00000008h-00000009h表示常量池的數量,由上圖可以知道其值為0x0018,十進位制為24個,但是對於常量池的數量 需要明確一點,常量池的數量是constant_pool_count-1,為什麼減一,是因為索引0表示class中的資料項不引用任何常量池中的常 量。

4)常量池
我們上面說了常量池中有不同型別的常量,下面就來看看TestClass.class的第一個常量,我們知道每個常量都有一個u1型別的tag標識來表示 常量的型別,上圖中0000000ah處的內容為0x0A,轉換成二級制是10,有上面的關於常量型別的描述可知tag為10的常量是Constant_Methodref_info,而Constant_Methodref_info的結夠如下圖所示:

其中class_index指向常量池中型別為CONSTANT_Class_info的常量,從TestClass的二進位制檔案結構中可以看出 class_index的值為0x0004(地址為0000000bh-0000000ch),也就是說指向第四個常量。

name_and_type_index指向常量池中型別為CONSTANT_NameAndType_info常量。從上圖可以看出name_and_type_index的值為0x0013,表示指向常量池中的第19個常量。

接下來又可以通過同樣的方法來找到常量池中的所有常量。不過JDK提供了一個方便的工具可以讓我們檢視常量池中所包含的常量。通過javap -verbose TestClass 即可得到所有常量池中的常量,截圖如下:

從上圖我們可以清楚的看到,TestClass中常量池有24個常量,不要忘記了第0個常量,因為第0個常量被用來表示 Class中的資料項不引用任何常量池中的常量。從上面的分析中我們得知TestClass的第一個常量表示方法,其中class_index指向的第四 個常量為java/lang/Object,name_and_type_index指向的第19個常量值為<init>:()V,從這裡可 以看出第一個表示方法的常量表示的是java編譯器生成的例項構造器方法。通過同樣的方法可以分析常量池的其它常量。OK,分析完常量池,我們接下來再分 析下access_flags。
5)u2 access_flags 表示類或者介面方面的訪問資訊,比如Class表示的是類還是介面,是否為public,static,final等。具體訪問標示的含義之前已經說過 了,下面我們就來看看TestClass的訪問標示。Class的訪問標示是從0000010dh-0000010e,期值為0x0021,根據前面說的 各種訪問標示的標誌位,我們可以知道:0x0021=0x0001|0x0020 也即ACC_PUBLIC 和 ACC_SUPER為真,其中ACC_PUBLIC大家好理解,ACC_SUPER是jdk1.2之後編譯的類都會帶有的標誌。

6)u2 this_class 表示類的索引值,用來表示類的全限定名稱,類的索引值如下圖所示:

從上圖可以清楚到看到,類索引值為0x0003,對應常量池的第三個常量,通過javap的結果,我們知道第三個常量為 CONSTANT_Class_info型別的常量,通過它可以知道類的全限定名稱為:com/ejushang/TestClass /TestClass

7)u2 super_class 表示當前類的父類的索引值,索引值所指向的常量池中型別為CONSTANT_Class_info的常量,父類的索引值如下圖所示,其值為0x0004, 檢視常量池的第四個常量,可知TestClass的父類的全限定名稱為:java/lang/Object

8)interfaces_count和  interfaces[interfaces_count]表示介面數量以及具體的每一個介面,TestClass的介面數量以及介面如下圖所示,其中 0x0001表示介面數量為1,而0x0005表示介面在常量池的索引值,找到常量池的第五個常量,其型別為CONSTANT_Class_info,其 值為:com/ejushang/TestClass/Super

9)fields_count 和 field_info, fields_count表示類中field_info表的數量,而field_info表示類的例項變數和類變數,這裡需要注意的是 field_info不包含從父類繼承過來的欄位,field_info的結構如下圖所示:

其中access_flags表示欄位的訪問標示,比如public,private,protected,static,final等,access_flags的取值如下圖所示:

其中name_index 和 descriptor_index都是常量池的索引值,分別表示欄位的名稱和欄位的描述符,欄位的名稱容易理解,但是欄位的描述符如何理解呢?其實在JVM 規範中,對於欄位的描述符規定如下圖所示:

其中大家需要關注一下上圖最後一行,它表示的是對一維陣列的描述符,對於String[][]的描述符將是[[ Ljava/lang/String,而對於int[][]的描述符為[[I。接下來的attributes_count以及 attribute_info分別表示屬性表的數量以及屬性表。下面我們還是以上面的TestClass為例,來看看TestClass的欄位表吧。

首先我們來看一下欄位的數量,TestClass的欄位的數量如下圖所示:

從上圖中可以看出TestClass有兩個欄位,檢視TestClass的原始碼可知,確實也只有兩個欄位,接下來我們看看第一個欄位,我們知道第一個欄位應該為private int staticVar,它在Class檔案中的二進位制表示如下圖所示:


其中0x001A表示訪問標示,通過檢視access_flags表可知,其為ACC_PRIVATE,ACC_STATIC,ACC_FINAL,接下 來0x0006和0x0007分別表示常量池中第6和第7個常量,通過檢視常量池可知,其值分別為:staticVar和I,其中staticVar為字 段名稱,而I為欄位的描述符,通過上面對描述符的解釋,I所描述的是int型別的變數,接下來0x0001表示staticVar這個欄位表中的屬性表的 數量,從上圖可以staticVar欄位對應的屬性表有1個,0x0008表示常量池中的第8個常量,檢視常量池可以得知此屬性為 ConstantValue屬性,而ConstantValue屬性的格式如下圖所示:

其中attribute_name_index表述屬性名的常量池索引,本例中為ConstantValue,而ConstantValue的 attribute_length固定長度為2,而constantValue_index表示常量池中的引用,本例中,其中為0x0009,檢視第9個 常量可以知道,它表示一個型別為CONSTANT_Integer_info的常量,其值為0。

上面說完了private static final int staticVar=0,下面我們接著說一下TestClass的private int instanceVar=0,在本例中對instanceVar的二進位制表示如下圖所示:


其中0x0002表示訪問標示為ACC_PRIVATE,0x000A表示欄位的名稱,它指向常量池中的第10個常量,檢視常量池可以知道欄位名稱為 instanceVar,而0x0007表示欄位的描述符,它指向常量池中的第7個常量,檢視常量池可以知道第7個常量為I,表示型別為 instanceVar的型別為I,最後0x0000表示屬性表的數量為0.

10)methods_count 和 method_info ,其中methods_count表示方法的數量,而method_info表示的方法表,其中方法表的結構如下圖所示:

從上圖可以看出method_info和field_info的結構是很類似的,方法表的access_flag的所有標誌位以及取值如下圖所示:

其中name_index和descriptor_index表示的是方法的名稱和描述符,他們分別是指向常量池的索引。這裡需要結解釋一下方法的描述 符,方法的描述符的結構為:(引數列表)返回值,比如public int instanceMethod(int param)的描述符為:(I)I,表示帶有一個int型別引數且返回值也為int型別的方法,接下來就是屬性數量以及屬性表了,方法表和欄位表雖然都有 屬性數量和屬性表,但是他們裡面所包含的屬性是不同。接下來我們就以TestClass來看一下方法表的二進位制表示。首先來看一下方法表數量,截圖如下:


從上圖可以看出方法表的數量為0x0002表示有兩個方法,接下來我們來分析第一個方法,我們首先來看一下TestClass的第一個方法的access_flag,name_index,descriptor_index,截圖如下:


從上圖可以知道access_flags為0x0001,從上面對access_flags標誌位的描述,可知方法的access_flags的取值為 ACC_PUBLIC,name_index為0x000B,檢視常量池中的第11個常量,知道方法的名稱為<init>,0x000C表示 descriptor_index表示常量池中的第12常量,其值為()V,表示<init>方法沒有引數和返回值,其實這是編譯器自動生成 的例項構造器方法。接下來的0x0001表示<init>方法的方法表有1個屬性,屬性截圖如下:

從上圖可以看出0x000D對應的常量池中的常量為Code,表示的方法的Code屬性,所以到這裡大家應該明白方法的那些程式碼是儲存在Class檔案方法表中的屬性表中的Code屬性中。接下來我們在分析一下Code屬性,Code屬性的結構如下圖所示:

其中attribute_name_index指向常量池中值為Code的常量,attribute_length的長度表示Code屬性表的長度(這裡 需要注意的時候長度不包括attribute_name_index和attribute_length的6個位元組的長度)。

max_stack表示最大棧深度,虛擬機器在執行時根據這個值來分配棧幀中運算元的深度,而max_locals代表了區域性變數表的儲存空間。

max_locals的單位為slot,slot是虛擬機器為區域性變數分配記憶體的最小單元,在執行時,對於不超過32位型別的資料型別,比如 byte,char,int等佔用1個slot,而double和Long這種64位的資料型別則需要分配2個slot,另外max_locals的值並 不是所有區域性變數所需要的記憶體數量之和,因為slot是可以重用的,當區域性變數超過了它的作用域以後,區域性變數所佔用的slot就會被重用。

code_length代表了位元組碼指令的數量,而code表示的時候位元組碼指令,從上圖可以知道code的型別為u1,一個u1型別的取值為0x00-0xFF,對應的十進位制為0-255,目前虛擬機器規範已經定義了200多條指令。

exception_table_length以及exception_table分別代表方法對應的異常資訊。

attributes_count和attribute_info分別表示了Code屬性中的屬性數量和屬性表,從這裡可以看出Class的檔案結構中,屬性表是很靈活的,它可以存在於Class檔案,方法表,欄位表以及Code屬性中。

接下來我們繼續以上面的例子來分析一下,從上面init方法的Code屬性的截圖中可以看出,屬性表的長度為0x00000026,max_stack的 值為0x0002,max_locals的取值為0x0001,code_length的長度為0x0000000A,那麼00000149h- 00000152h為位元組碼,接下來exception_table_length的長度為0x0000,而attribute_count的值為 0x0001,00000157h-00000158h的值為0x000E,它表示常量池中屬性的名稱,檢視常量池得知第14個常量的值為 LineNumberTable,LineNumberTable用於描述java原始碼的行號和位元組碼行號的對應關係,它不是執行時必需的屬性,如果通 過-g:none的編譯器引數來取消生成這項資訊的話,最大的影響就是異常發生的時候,堆疊中不能顯示出出錯的行號,除錯的時候也不能按照原始碼來設定斷 點,接下來我們再看一下LineNumberTable的結構如下圖所示:

其中attribute_name_index上面已經提到過,表示常量池的索引,attribute_length表示屬性長度,而start_pc和 line_number分表表示位元組碼的行號和原始碼的行號。本例中LineNumberTable屬性的位元組流如下圖所示:

上面分析完了TestClass的第一個方法,通過同樣的方式我們可以分析出TestClass的第二個方法,截圖如下:

其中access_flags為0x0001,name_index為0x000F,descriptor_index為0x0010,通過檢視常量池可 以知道此方法為public int instanceMethod(int param)方法。通過和上面類似的方法我們可以知道instanceMethod的Code屬性為下圖所示:

最後我們來分析一下,Class檔案的屬性,從00000191h-00000199h為Class檔案中的屬性表,其中0x0011表示屬性的名稱,檢視常量池可以知道屬性名稱為SourceFile,我們再來看看SourceFile的結構如下圖所示:

其中attribute_length為屬性的長度,sourcefile_index指向常量池中值為原始碼檔名稱的常量,在本例中SourceFile屬性截圖如下:


其中attribute_length為0x00000002表示長度為2個位元組,而soucefile_index的值為0x0012,檢視常量池的第18個常量可以知道原始碼檔案的名稱為TestClass.java

作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要這是一個我的QQ群架構華山論劍:836442475,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!

相關文章