從一個class檔案深入理解Java位元組碼結構
前言
我們都知道,Java程式最終是轉換成class檔案執行在虛擬機器上的,那麼class檔案是個怎樣的結構,虛擬機器又是如何處理去執行class檔案裡面的內容呢,這篇文章帶你深入理解Java位元組碼中的結構。
1.Demo原始碼
首先,編寫一個簡單的Java原始碼:
package com.april.test;
public class Demo {
private int num = 1;
public int add() {
num = num + 2;
return num;
}
}
這段程式碼很簡單,只有一個成員變數num
add()
。
2.位元組碼
要執行一段Java原始碼,必須先將原始碼轉換為class檔案,class檔案就是編譯器編譯之後供虛擬機器解釋執行的二進位制位元組碼檔案,可以通過IDE工具
或者命令列
去將原始碼編譯成class檔案。這裡我們使用命令列去操作,執行下面命令:
javac Demo.java
就會生成一個Demo.class
檔案。
我們開啟這個Demo.class檔案看下。這裡用到的是Notepad++
,需要安裝一個HEX-Editor
外掛。
3.class檔案反編譯java檔案
在分析class檔案之前,我們先來看下將這個Demo.class反編譯回Demo.java的結果,如下圖所示:
可以看到,回編譯的原始碼比編寫的程式碼多了一個
空的建構函式
和this關鍵字
,為什麼呢?先放下這個疑問,看完這篇分析,相信你就知道答案了。
4.位元組碼結構
從上面的位元組碼檔案中我們可以看到,裡面就是一堆的16進位制位元組。那麼該如何解讀呢?別急,我們先來看一張表:
型別 | 名稱 | 說明 | 長度 |
---|---|---|---|
u4 | magic | 魔數,識別Class檔案格式 | 4個位元組 |
u2 | minor_version | 副版本號 | 2個位元組 |
u2 | major_version | 主版本號 | 2個位元組 |
u2 | constant_pool_count | 常量池計算器 | 2個位元組 |
cp_info | constant_pool | 常量池 | n個位元組 |
u2 | access_flags | 訪問標誌 | 2個位元組 |
u2 | this_class | 類索引 | 2個位元組 |
u2 | super_class | 父類索引 | 2個位元組 |
u2 | interfaces_count | 介面計數器 | 2個位元組 |
u2 | interfaces | 介面索引集合 | 2個位元組 |
u2 | fields_count | 欄位個數 | 2個位元組 |
field_info | fields | 欄位集合 | n個位元組 |
u2 | methods_count | 方法計數器 | 2個位元組 |
method_info | methods | 方法集合 | n個位元組 |
u2 | attributes_count | 附加屬性計數器 | 2個位元組 |
attribute_info | attributes | 附加屬性集合 | n個位元組 |
這是一張Java位元組碼總的結構表,我們按照上面的順序逐一進行解讀就可以了。
首先,我們來說明一下:class檔案只有兩種資料型別:無符號數
和表
。如下表所示:
資料型別 | 定義 | 說明 |
---|---|---|
無符號數 | 無符號數可以用來描述數字、索引引用、數量值或按照utf-8編碼構成的字串值。 | 其中無符號數屬於基本的資料型別。 以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組 |
表 | 表是由多個無符號數或其他表構成的複合資料結構。 | 所有的表都以“_info”結尾。 由於表沒有固定長度,所以通常會在其前面加上個數說明。 |
實際上整個class檔案就是一張表,其結構就是上面的表一了。
那麼我們現在再來看錶一中的型別那一列,也就很簡單了:
型別 | 說明 | 長度 |
---|---|---|
u1 | 1個位元組 | 1 |
u2 | 2個位元組 | 2 |
u4 | 4個位元組 | 4 |
u8 | 8個位元組 | 8 |
cp_info | 常量表 | n |
field_info | 欄位表 | n |
method_info | 方法表 | n |
attribute_info | 屬性表 | n |
上面各種具體的表的資料結構後面會詳細說明,這裡暫且不表。
好了,現在我們開始對那一堆的16進位制進行解讀。
4.1 魔數
從上面的總的結構圖中可以看到,開頭的4個位元組表示的是魔數,其值為:
嗯,其值為0XCAFE BABE
。CAFE BABE??What the fxxk?
好了,那麼什麼是魔數呢?魔數就是用來區分檔案型別的一種標誌,一般都是用檔案的前幾個位元組來表示。比如0XCAFE BABE
表示的是class檔案,那麼為什麼不是用檔名字尾來進行判斷呢?因為檔名字尾容易被修改啊,所以為了保證檔案的安全性,將檔案型別寫在檔案內部可以保證不被篡改。
再來說說為什麼class檔案用的是CAFE BABE呢,看到這個大概你就懂了。
4.2 版本號
緊跟著魔數後面的4位就是版本號了,同樣也是4個位元組,其中前2個位元組表示副版本號
,後2個位元組
表示主版本號
。再來看看我們Demo位元組碼中的值:
前面兩個位元組是0x0000
,也就是其值為0;
後面兩個位元組是0x0034
,也就是其值為52.
所以上面的程式碼就是52.0版本來編譯的,也就是jdk1.8.0
。
4.3 常量池
4.3.1 常量池容量計數器
接下來就是常量池了。由於常量池的數量不固定,時長時短,所以需要放置兩個位元組來表示常量池容量計數值。Demo的值為:
其值為0x0013
,掐指一算,也就是19。
需要注意的是,這實際上只有18項常量。為什麼呢?
通常我們寫程式碼時都是從0開始的,但是這裡的常量池卻是從1開始,因為它把第0項常量空出來了。這是為了在於滿足後面某些指向常量池的索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的含義,這種情況可用索引值0來表示。
Class檔案中只有常量池的容量計數是從1開始的,對於其他集合型別,包括介面索引集合、欄位表集合、方法表集合等的容量計數都與一般習慣相同,是從0開始的。
4.3.2 字面量和符號引用
在對這些常量解讀前,我們需要搞清楚幾個概念。
常量池主要存放兩大類常量:字面量
和符號引用
。如下表:
常量 | 具體的常量 |
---|---|
字面量 | 文字字串 |
宣告為final的常量值 | |
符號引用 | 類和介面的全限定名 |
欄位的名稱和描述符 | |
方法的名稱和描述符 |
4.3.2.1 全限定名
com/april/test/Demo
這個就是類的全限定名,僅僅是把包名的”.”替換成”/”,為了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個“;”表示全限定名結束。
4.3.2.2 簡單名稱
簡單名稱是指沒有型別和引數修飾的方法或者欄位名稱,上面例子中的類的add()
方法和num
欄位的簡單名稱分別是add
和num
。
4.3.2.3 描述符
描述符的作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。根據描述符規則,基本資料型別(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 | 物件型別,如Ljava/lang/Object |
對於陣列型別,每一維度將使用一個前置的[
字元來描述,如一個定義為java.lang.String[][]
型別的二維陣列,將被記錄為:[[Ljava/lang/String;
,,一個整型陣列int[]
被記錄為[I
。
用描述符來描述方法時,按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組小括號“( )”之內。如方法java.lang.String toString()
的描述符為( ) LJava/lang/String;
,方法int abc(int[] x, int y)
的描述符為([II) I
。
4.3.3 常量型別和結構
常量池中的每一項都是一個表,其專案型別共有14種,如下表格所示:
型別 | 標誌 | 描述 |
---|---|---|
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 | 欄位或方法的符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法控制代碼 |
CONSTANT_MothodType_info | 16 | 標誌方法型別 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法呼叫點 |
這14種類型的結構各不相同,如下表格所示:
從上面的表格可以看到,雖然每一項的結構都各不相同,但是他們有個共同點,就是每一項的第一個位元組都是一個標誌位,標識這一項是哪種型別的常量。
4.3.4 常量解讀
好了,我們進入這18項常量的解讀,首先是第一個常量,看下它的標誌位是啥:
其值為0x0a
,即10,查上面的表格可知,其對應的專案型別為CONSTANT_Methodref_info
,即類中方法的符號引用。其結構為:
即後面4個位元組都是它的內容,分別為兩個索引項:
其中前兩位的值為0x0004
,即4,指向常量池第4項的索引;
後兩位的值為0x000f
,即15,指向常量池第15項的索引。
至此,第一個常量就解讀完畢了。
我們再來看下第二個常量:
其標誌位的值為0x09
,即9,查上面的表格可知,其對應的專案型別為CONSTANT_Fieldref_info
,即欄位的符號引用。其結構為:
同樣也是4個位元組,前後都是兩個索引。分別指向第4項的索引和第10項的索引。
後面還有16項常量就不一一去解讀了,因為整個常量池還是挺長的:
你看,這麼長的一大段16進位制,看的我都快瞎了:
實際上,我們只要敲一行簡單的命令:
javap -verbose Demo.class
其中部分的輸出結果為:
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/april/test/Demo.num:I
#3 = Class #17 // com/april/test/Demo
#4 = Class #18 // java/lang/Object
#5 = Utf8 num
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Demo.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // num:I
#17 = Utf8 com/april/test/Demo
#18 = Utf8 java/lang/Object
你看,一家大小,齊齊整整,全都出來了。
但是,通過我們手動去分析才知道這個結果是怎麼出來的,要知其然知其所以然嘛~
4.4 訪問標誌
常量池後面就是訪問標誌,用兩個位元組來表示,其標識了類或者介面的訪問資訊,比如:該Class檔案是類還是介面,是否被定義成public
,是否是abstract
,如果是類,是否被宣告成final
等等。各種訪問標誌如下所示:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為Public型別 |
ACC_FINAL | 0x0010 | 是否被宣告為final,只有類可以設定 |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial位元組碼指令的新語義,JDK1.0.2之後編譯出來的類的這個標誌預設為真 |
ACC_INTERFACE | 0x0200 | 標誌這是一個介面 |
ACC_ABSTRACT | 0x0400 | 是否為abstract型別,對於介面或者抽象類來說,次標誌值為真,其他型別為假 |
ACC_SYNTHETIC | 0x1000 | 標誌這個類並非由使用者程式碼產生 |
ACC_ANNOTATION | 0x2000 | 標誌這是一個註解 |
ACC_ENUM | x4000 | 標誌這是一個列舉 |
再來看下我們Demo位元組碼中的值:
其值為:0x0021
,是0x0020
和0x0001
的並集,即這是一個Public
的類,再回頭看看我們的原始碼。
確認過眼神,我遇上對的了。
4.5 類索引、父類索引、介面索引
訪問標誌後的兩個位元組就是類索引;
類索引後的兩個位元組就是父類索引;
父類索引後的兩個位元組則是介面索引計數器。
通過這三項,就可以確定了這個類的繼承關係了。
4.5.1 類索引
我們直接來看下Demo位元組碼中的值:
類索引的值為0x0003
,即為指向常量池中第三項的索引。你看,這裡用到了常量池中的值了。
我們回頭翻翻常量池中的第三項:
#3 = Class #17 // com/april/test/Demo
通過類索引我們可以確定到類的全限定名。
4.5.2 父類索引
從上圖看到,父類索引的值為0x0004
,即常量池中的第四項:
#4 = Class #18 // java/lang/Object
這樣我們就可以確定到父類的全限定名。
可以看到,如果我們沒有繼承任何類,其預設繼承的是java/lang/Object
類。
同時,由於Java不支援多繼承
,所以其父類只有一個。
4.5.3 介面計數器
從上圖看到,介面索引個數的值為0x0000
,即沒有任何介面索引,我們demo的原始碼也確實沒有去實現任何介面。
4.5.4 介面索引集合
由於我們demo的原始碼沒有去實現任何介面,所以介面索引集合就為空了,不佔地方,嘻嘻。
可以看到,由於Java支援多介面
,因此這裡設計成了介面計數器和介面索引集合來實現。
4.6 欄位表
介面計數器或介面索引集合後面就是欄位表了。
欄位表用來描述類或者介面中宣告的變數。這裡的欄位包含了類級別變數以及例項變數,但是不包括方法內部宣告的區域性變數。
4.6.1 欄位表計數器
同樣,其前面兩個位元組用來表示欄位表的容量,看下demo位元組碼中的值:
其值為0x0001
,表示只有一個欄位。
4.6.2 欄位表訪問標誌
我們知道,一個欄位可以被各種關鍵字去修飾,比如:作用域修飾符(public、private、protected
)、static
修飾符、final
修飾符、volatile
修飾符等等。因此,其可像類的訪問標誌那樣,使用一些標誌來標記欄位。欄位的訪問標誌有如下這些:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 欄位是否為public |
ACC_PRIVATE | 0x0002 | 欄位是否為private |
ACC_PROTECTED | 0x0004 | 欄位是否為protected |
ACC_STATIC | 0x0008 | 欄位是否為static |
ACC_FINAL | 0x0010 | 欄位是否為final |
ACC_VOLATILE | 0x0040 | 欄位是否為volatile |
ACC_TRANSTENT | 0x0080 | 欄位是否為transient |
ACC_SYNCHETIC | 0x1000 | 欄位是否為由編譯器自動產生 |
ACC_ENUM | 0x4000 | 欄位是否為enum |
4.6.3 欄位表結構
欄位表作為一個表,同樣有他自己的結構:
型別 | 名稱 | 含義 | 數量 |
---|---|---|---|
u2 | access_flags | 訪問標誌 | 1 |
u2 | name_index | 欄位名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attributes_count | 屬性計數器 | 1 |
attribute_info | attributes | 屬性集合 | attributes_count |
4.6.4 欄位表解讀
我們先來回顧一下我們demo原始碼中的欄位:
private int num = 1;
由於只有一個欄位,還是比較簡單的,直接看demo位元組碼中的值:
訪問標誌的值為0x0002
,查詢上面欄位訪問標誌的表格,可得欄位為private
;
欄位名索引的值為0x0005
,查詢常量池中的第5項,可得:
#5 = Utf8 num
描述符索引的值為0x0006
,查詢常量池中的第6項,可得:
#6 = Utf8 I
屬性計數器的值為0x0000
,即沒有任何的屬性。
確認過眼神,我遇上對的了。
至此,欄位表解讀完成。
4.6.5 注意事項
- 欄位表集合中不會列出從父類或者父介面中繼承而來的欄位。
- 內部類中為了保持對外部類的訪問性,會自動新增指向外部類例項的欄位。
- 在Java語言中欄位是無法過載的,兩個欄位的資料型別,修飾符不管是否相同,都必須使用不一樣的名稱,但是對於位元組碼來講,如果兩個欄位的描述符不一致,那欄位重名就是合法的.
4.7 方法表
欄位表後就是方法表了。
4.7.1 方法表計數器
前面兩個位元組依然用來表示方法表的容量,看下demo位元組碼中的值:
其值為0x0002
,即有2個方法。
4.7.2 方法表訪問標誌
跟欄位表一樣,方法表也有訪問標誌,而且他們的標誌有部分相同,部分則不同,方法表的具體訪問標誌如下:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否為public |
ACC_PRIVATE | 0x0002 | 方法是否為private |
ACC_PROTECTED | 0x0004 | 方法是否為protected |
ACC_STATIC | 0x0008 | 方法是否為static |
ACC_FINAL | 0x0010 | 方法是否為final |
ACC_SYHCHRONRIZED | 0x0020 | 方法是否為synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是有編譯器產生的方法 |
ACC_VARARGS | 0x0080 | 方法是否接受引數 |
ACC_NATIVE | 0x0100 | 方法是否為native |
ACC_ABSTRACT | 0x0400 | 方法是否為abstract |
ACC_STRICTFP | 0x0800 | 方法是否為strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是有編譯器自動產生的 |
4.7.3 方法表結構
方法表的結構實際跟欄位表是一樣的,方法表結構如下:
型別 | 名稱 | 含義 | 數量 |
---|---|---|---|
u2 | access_flags | 訪問標誌 | 1 |
u2 | name_index | 方法名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attributes_count | 屬性計數器 | 1 |
attribute_info | attributes | 屬性集合 | attributes_count |
4.7.4 屬性解讀
還是先回顧一下Demo中的原始碼:
public int add() {
num = num + 2;
return num;
}
只有一個自定義的方法。但是上面方法表計數器明明是2個,這是為啥呢?
這是因為它包含了預設的構造方法
,我們來看下下面的分析就懂了,先看下Demo位元組碼中的值:
這是第一個方法表,我們來解讀一下這裡面的16進位制:
訪問標誌的值為0x0001
,查詢上面欄位訪問標誌的表格,可得欄位為public;
方法名索引的值為0x0007
,查詢常量池中的第7項,可得:
#7 = Utf8 <init>
這個名為<init>
的方法實際上就是預設的構造方法
了。
描述符索引的值為0x0008
,查詢常量池中的第8項,可得:
#8 = Utf8 ()V
注:描述符不熟悉的話可以回頭看看4.3.2.3的內容。
屬性計數器的值為0x0001
,即這個方法表有一個屬性。
屬性計數器後面就是屬性表了,由於只有一個屬性,所以這裡也只有一個屬性表。
由於涉及到屬性表,這裡簡單說下,下一節會詳細介紹。
屬性表的前兩個位元組是屬性名稱索引,這裡的值為0x0009
,查下常量池中的第9項:
#9 = Utf8 Code
即這是一個Code屬性
,我們方法裡面的程式碼就是存放在這個Code屬性裡面。相關細節暫且不表。下一節會詳細介紹Code屬性。
先跳過屬性表,我們再來看下第二個方法:
訪問標誌的值為0x0001
,查詢上面欄位訪問標誌的表格,可得欄位為public;
方法名索引的值為0x000b
,查詢常量池中的第11項,可得:
#11 = Utf8 add
描述符索引的值為0x000c,查詢常量池中的第12項,可得:
#12 = Utf8 ()I
屬性計數器的值為0x0001
,即這個方法表有一個屬性。
屬性名稱索引的值同樣也是0x0009
,即這是一個Code屬性。
可以看到,第二個方法表就是我們自定義的add()
方法了。
4.7.5 注意事項
- 如果父類方法在子類中沒有被重寫(
Override
),方法表集合中就不會出現父類的方法。 - 編譯器可能會自動新增方法,最典型的便是類構造方法(靜態構造方法)
<client>
方法和預設例項構造方法<init>
方法。 - 在Java語言中,要過載(
Overload
)一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特徵簽名,特徵簽名就是一個方法中各個引數在常量池中的欄位符號引用的集合,也就是因為返回值不會包含在特徵簽名之中,因此Java語言裡無法僅僅依靠返回值的不同來對一個已有方法進行過載。但在Class檔案格式中,特徵簽名的範圍更大一些,只要描述符不是完全一致的兩個方法就可以共存。也就是說,如果兩個方法有相同的名稱和特徵簽名,但返回值不同,那麼也是可以合法共存於同一個class檔案中。
4.8 屬性表
前面說到了屬性表,現在來重點看下。屬性表不僅在方法表有用到,欄位表和Class檔案中也會用得到。本篇文章中用到的例子在欄位表中的屬性個數為0,所以也沒涉及到;在方法表中用到了2次,都是Code屬性;至於Class檔案,在末尾時會講到,這裡就先不說了。
4.8.1 屬性型別
屬性表實際上可以有很多型別,上面看到的Code屬性只是其中一種,下面這些是虛擬機器中預定義的屬性:
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java程式碼編譯成的位元組碼指令 |
ConstantValue | 欄位表 | final關鍵字定義的常量池 |
Deprecated | 類,方法,欄位表 | 被宣告為deprecated的方法和欄位 |
Exceptions | 方法表 | 方法丟擲的異常 |
EnclosingMethod | 類檔案 | 僅當一個類為區域性類或者匿名類是才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法 |
InnerClass | 類檔案 | 內部類列表 |
LineNumberTable | Code屬性 | Java原始碼的行號與位元組碼指令的對應關係 |
LocalVariableTable | Code屬性 | 方法的區域性便狼描述 |
StackMapTable | Code屬性 | JDK1.6中新增的屬性,供新的型別檢查檢驗器檢查和處理目標方法的區域性變數和運算元有所需要的類是否匹配 |
Signature | 類,方法表,欄位表 | 用於支援泛型情況下的方法簽名 |
SourceFile | 類檔案 | 記錄原始檔名稱 |
SourceDebugExtension | 類檔案 | 用於儲存額外的除錯資訊 |
Synthetic | 類,方法表,欄位表 | 標誌方法或欄位為編譯器自動生成的 |
LocalVariableTypeTable | 類 | 使用特徵簽名代替描述符,是為了引入泛型語法之後能描述泛型引數化型別而新增 |
RuntimeVisibleAnnotations | 類,方法表,欄位表 | 為動態註解提供支援 |
RuntimeInvisibleAnnotations | 表,方法表,欄位表 | 用於指明哪些註解是執行時不可見的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用與RuntimeVisibleAnnotations屬性類似,只不過作用物件為方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用與RuntimeInvisibleAnnotations屬性類似,作用物件哪個為方法引數 |
AnnotationDefault | 方法表 | 用於記錄註解類元素的預設值 |
BootstrapMethods | 類檔案 | 用於儲存invokeddynamic指令引用的引導方式限定符 |
4.8.2 屬性表結構
屬性表的結構比較靈活,各種不同的屬性只要滿足以下結構即可:
型別 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名索引 |
u2 | attribute_length | 1 | 屬性長度 |
u1 | info | attribute_length | 屬性表 |
即只需說明屬性的名稱
以及佔用位數的長度
即可,屬性表具體的結構
可以去自定義
。
4.8.3 部分屬性詳解
下面針對部分常見的一些屬性進行詳解
4.8.3.1 Code屬性
前面我們看到的屬性表都是Code屬性,我們這裡重點來看下。
Code屬性就是存放方法體裡面的程式碼,像介面或者抽象方法
,他們沒有具體的方法體,因此也就不會有Code屬性了。
4.8.3.1.1 Code屬性表結構
先來看下Code屬性表的結構,如下圖:
型別 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名索引 |
u4 | attribute_length | 1 | 屬性長度 |
u2 | max_stack | 1 | 運算元棧深度的最大值 |
u2 | max_locals | 1 | 區域性變量表所需的存續空間 |
u4 | code_length | 1 | 位元組碼指令的長度 |
u1 | code | code_length | 儲存位元組碼指令 |
u2 | exception_table_length | 1 | 異常表長度 |
exception_info | exception_table | exception_length | 異常表 |
u2 | attributes_count | 1 | 屬性集合計數器 |
attribute_info | attributes | attributes_count | 屬性集合 |
可以看到:Code屬性表的前兩項跟屬性表是一致的,即Code屬性表遵循屬性表的結構,後面那些則是他自定義的結構。
4.8.3.1.2 Code屬性解讀
同樣,解讀Code屬性只需按照上面的表格逐一解讀即可。
我們先來看下第一個方法表中的Code屬性:
屬性名索引的值為0x0009
,上面也說過了,這是一個Code屬性;
屬性長度的值為0x00000026
,即長度為38,注意,這裡的長度是指後面自定義的屬性長度,不包括屬性名索引和屬性長度這兩個所佔的長度,因為這哥倆佔的長度都是固定6個位元組了,所以往後38個位元組都是Code屬性的內容;
max_stack的值為0x0002
,即運算元棧深度的最大值為2;
max_locals的值為0x0001
,即區域性變量表所需的儲存空間為1;max_locals的單位是Slot
,Slot是虛擬機器為區域性變數分配記憶體所使用的最小單位。
code_length的值為0x00000000a
,即位元組碼指令的10;
code的值為0x2a b7 00 01 2a 04 b5 00 02 b1
,這裡的值就代表一系列的位元組碼指令。一個位元組代表一個指令,一個指令可能有引數也可能沒引數,如果有引數,則其後面位元組碼就是他的引數;如果沒引數,後面的位元組碼就是下一條指令。
這裡我們來解讀一下這些指令,文末最後的附錄附有Java虛擬機器位元組碼指令表,可以通過指令表來查詢指令的含義。
2a
指令,查表可得指令為aload_0
,其含義為:將第0個Slot中為reference型別的本地變數推送到運算元棧頂。b7
指令,查表可得指令為invokespecial
,其含義為:將運算元棧頂的reference型別的資料所指向的物件作為方法接受者,呼叫此物件的例項構造器方法、private方法或者它的父類的方法。其後面緊跟著的2個位元組即指向其具體要呼叫的方法。00 01
,指向常量池中的第1項,查詢上面的常量池可得:#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
。即這是要呼叫預設構造方法<init>
。2a
指令,同第1個。04
指令,查表可得指令為iconst_1
,其含義為:將int型常量值1推送至棧頂。b5
指令,查表可得指令為putfield
,其含義為:為指定的類的例項域賦值。其後的2個位元組為要賦值的例項。00 02
,指向常量池中的第2項,查詢上面的常量池可得:#2 = Fieldref #3.#16 // com/april/test/Demo.num:I
。即這裡要將num這個欄位賦值為1。b5
指令,查表可得指令為return
,其含義為:返回此方法,並且返回值為void。這條指令執行完後,當前的方法也就結束了。
所以,上面的指令簡單點來說就是,呼叫預設的構造方法,並初始化num的值為1。
同時,可以看到,這些操作都是基於棧來完成的。
如果要逐字逐字的去查每一個指令的意思,那是相當的麻煩,大概要查到猴年馬月吧。實際上,只要一行命令,就能將這樣位元組碼轉化為指令了,還是javap命令哈:
javap -verbose Demo.class
擷取部分輸出結果:
public com.april.test.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num:I
9: return
LineNumberTable:
line 7: 0
line 8: 4
看看,那是相當的簡單。關於位元組碼指令,就到此為止了。繼續往下看。
exception_table_length的值為0x0000
,即異常表長度為0,所以其異常表也就沒有了;
attributes_count的值為0x0001
,即code屬性表裡面還有一個其他的屬性表,後面就是這個其他屬性的屬性表了;
所有的屬性都遵循屬性表的結構,同樣,這裡的結構也不例外。
前兩個位元組為屬性名索引,其值為0x000a,
檢視常量池中的第10項:
#10 = Utf8 LineNumberTable
即這是一個LineNumberTable
屬性。LineNumberTable
屬性先跳過,具體可以看下一小節。
再來看下第二個方法表中的的Code屬性:
屬性名索引的值同樣為0x0009
,所以,這也是一個Code屬性;
屬性長度的值為0x0000002b
,即長度為43;
max_stack的值為0x0003
,即運算元棧深度的最大值為3;
max_locals的值為0x0001
,即區域性變量表所需的儲存空間為1;
code_length的值為0x00000000f
,即位元組碼指令的15;
code的值為0x2a 2a b4 20 02 05 60 b5 20 02 2a b4 20 02 ac
,使用javap命令,可得:
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: aload_0
2: getfield #2 // Field num:I
5: iconst_2
6: iadd
7: putfield #2 // Field num:I
10: aload_0
11: getfield #2 // Field num:I
14: ireturn
LineNumberTable:
line 11: 0
line 12: 10
可以看到,這就是我們自定義的add()方法;
exception_table_length的值為0x0000
,即異常表長度為0,所以其異常表也沒有;
attributes_count的值為0x0001
,即code屬性表裡面還有一個其他的屬性表;
屬性名索引值為0x000a
,即這同樣也是一個LineNumberTable
屬性,LineNumberTable
屬性看下一小節。
4.8.3.2 LineNumberTable屬性
LineNumberTable屬性是用來描述Java原始碼行號
與位元組碼行號
之間的對應關係。
4.8.3.2.1 LineNumberTable屬性表結構
型別 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名索引 |
u4 | attribute_length | 1 | 屬性長度 |
u2 | line_number_table_length | 1 | 行號表長度 |
line_number_info | line_number_table | line_number_table_length | 行號表 |
line_number_info(行號表),其長度為4個位元組,前兩個為start_pc
,即位元組碼行號
;後兩個為line_number
,即Java原始碼行號
。
4.8.3.2.2 LineNumberTable屬性解讀
前面出現了兩個LineNumberTable屬性,先看第一個:
attributes_count的值為0x0001
,即code屬性表裡面還有一個其他的屬性表;
屬性名索引值為0x000a
,檢視常量池中的第10項:
#10 = Utf8 LineNumberTable
即這是一個LineNumberTable
屬性。
attribute_length的值為0x00 00 00 0a
,即其長度為10,後面10個位元組的都是LineNumberTable
屬性的內容;
line_number_table_length的值為0x0002
,即其行號表長度長度為2,即有兩個行號表;
第一個行號表其值為0x00 00 00 07
,即位元組碼第0行對應Java原始碼第7行;
第二個行號表其值為0x00 04 00 08
,即位元組碼第4行對應Java原始碼第8行。
同樣,使用javap
命令也能看到:
LineNumberTable:
line 7: 0
line 8: 4
第二個LineNumberTable屬性為:
這裡就不逐一看了,同樣使用javap
命令可得:
LineNumberTable:
line 11: 0
line 12: 10
所以這些行號是有什麼用呢?當程式丟擲異常時,我們就可以看到報錯的行號了,這利於我們debug;使用斷點時,也是根據原始碼的行號來設定的。
4.8.3.2 SourceFile屬性
前面將常量池、欄位集合、方法集合等都解讀完了。最終剩下的就是一些附加屬性了。
先來看看剩餘還未解讀的位元組碼:
同樣,前面2個位元組表示附加屬性計算器,其值為0x0001
,即還有一個附加屬性。
最後這一個屬性就是SourceFile屬性
,即原始碼檔案屬性。
先來看看其結構:
4.8.3.2.1 SourceFile屬性結構
型別 | 名稱 | 數量 | 含義 |
---|---|---|---|
u2 | attribute_name_index | 1 | 屬性名索引 |
u4 | attribute_length | 1 | 屬性長度 |
u2 | sourcefile_index | 1 | 原始碼檔案索引 |
可以看到,其長度總是固定的8個位元組。
4.8.3.2.2 SourceFile屬性解讀
屬性名索引的值為0x000d
,即常量池中的第13項,查詢可得:
#13 = Utf8 SourceFile
屬性長度的值為0x00 00 00 02
,即長度為2;
原始碼檔案索引的值為0x000e
,即常量池中的第14項,查詢可得:
#14 = Utf8 Demo.java
所以,我們能夠從這裡知道,這個Class檔案的原始碼檔名稱為Demo.java。同樣,當丟擲異常時,可以通過這個屬性定位到報錯的檔案。
至此,上面的位元組碼就完全解讀完畢了。
4.8.4 其他屬性
Java虛擬機器中預定義的屬性有20多個,這裡就不一一介紹了,通過上面幾個屬性的介紹,只要領會其精髓,其他屬性的解讀也是易如反掌。
5.總結
通過手動去解讀位元組碼檔案,終於大概瞭解到其構成和原理了。斷斷續續寫了比較長的時間,終於寫完了,撒花~
實際上,我們可以使用各種工具來幫我們去解讀位元組碼檔案,