1. 程式人生 > >從一個class檔案深入理解Java位元組碼結構

從一個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外掛。
1.位元組碼-完整版.png

3.class檔案反編譯java檔案

在分析class檔案之前,我們先來看下將這個Demo.class反編譯回Demo.java的結果,如下圖所示:
原始碼與class轉java對比.png


可以看到,回編譯的原始碼比編寫的程式碼多了一個空的建構函式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進位制進行解讀。
b.jpg

4.1 魔數

從上面的總的結構圖中可以看到,開頭的4個位元組表示的是魔數,其值為:
2.位元組碼-魔數.png
嗯,其值為0XCAFE BABE。CAFE BABE??What the fxxk?
好了,那麼什麼是魔數呢?魔數就是用來區分檔案型別的一種標誌,一般都是用檔案的前幾個位元組來表示。比如0XCAFE BABE表示的是class檔案,那麼為什麼不是用檔名字尾來進行判斷呢?因為檔名字尾容易被修改啊,所以為了保證檔案的安全性,將檔案型別寫在檔案內部可以保證不被篡改。
再來說說為什麼class檔案用的是CAFE BABE呢,看到這個大概你就懂了。
java.jpg

4.2 版本號

緊跟著魔數後面的4位就是版本號了,同樣也是4個位元組,其中前2個位元組表示副版本號,後2個位元組
表示主版本號。再來看看我們Demo位元組碼中的值:
3.位元組碼-版本號.png
前面兩個位元組是0x0000,也就是其值為0;
後面兩個位元組是0x0034,也就是其值為52.
所以上面的程式碼就是52.0版本來編譯的,也就是jdk1.8.0

4.3 常量池

4.3.1 常量池容量計數器

接下來就是常量池了。由於常量池的數量不固定,時長時短,所以需要放置兩個位元組來表示常量池容量計數值。Demo的值為:
4.位元組碼-常量池容量計數值.png
其值為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欄位的簡單名稱分別是addnum

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種類型的結構各不相同,如下表格所示:
常量池中常量項的結構總表.png

從上面的表格可以看到,雖然每一項的結構都各不相同,但是他們有個共同點,就是每一項的第一個位元組都是一個標誌位,標識這一項是哪種型別的常量。

4.3.4 常量解讀

好了,我們進入這18項常量的解讀,首先是第一個常量,看下它的標誌位是啥:
5.位元組碼-第一個常量的標誌位.png
其值為0x0a,即10,查上面的表格可知,其對應的專案型別為CONSTANT_Methodref_info,即類中方法的符號引用。其結構為:
CONSTANT_Methodref_info的結構.png
即後面4個位元組都是它的內容,分別為兩個索引項:
6.位元組碼-第一個常量的專案.png
其中前兩位的值為0x0004,即4,指向常量池第4項的索引;
後兩位的值為0x000f,即15,指向常量池第15項的索引。
至此,第一個常量就解讀完畢了。
我們再來看下第二個常量:
7.位元組碼-第二個常量.png
其標誌位的值為0x09,即9,查上面的表格可知,其對應的專案型別為CONSTANT_Fieldref_info,即欄位的符號引用。其結構為:
CONSTANT_Fieldref_info的結構.png
同樣也是4個位元組,前後都是兩個索引。分別指向第4項的索引和第10項的索引。

後面還有16項常量就不一一去解讀了,因為整個常量池還是挺長的:
8.位元組碼-所有常量.png

你看,這麼長的一大段16進位制,看的我都快瞎了:
你說什麼,我沒帶眼鏡聽不清.jpg

實際上,我們只要敲一行簡單的命令:

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位元組碼中的值:
9.位元組碼-訪問標誌.png
其值為:0x0021,是0x00200x0001的並集,即這是一個Public的類,再回頭看看我們的原始碼。
確認過眼神,我遇上對的了。

4.5 類索引、父類索引、介面索引

訪問標誌後的兩個位元組就是類索引;
類索引後的兩個位元組就是父類索引;
父類索引後的兩個位元組則是介面索引計數器。
通過這三項,就可以確定了這個類的繼承關係了。

4.5.1 類索引

我們直接來看下Demo位元組碼中的值:
10.位元組碼-類索引.png
類索引的值為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位元組碼中的值:
11.位元組碼-欄位表容量計數器.png
其值為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位元組碼中的值:
12.位元組碼-欄位表.png
訪問標誌的值為0x0002,查詢上面欄位訪問標誌的表格,可得欄位為private
欄位名索引的值為0x0005,查詢常量池中的第5項,可得:

   #5 = Utf8               num

描述符索引的值為0x0006,查詢常量池中的第6項,可得:

   #6 = Utf8               I

屬性計數器的值為0x0000,即沒有任何的屬性。

確認過眼神,我遇上對的了。

至此,欄位表解讀完成。

4.6.5 注意事項

  1. 欄位表集合中不會列出從父類或者父介面中繼承而來的欄位。
  2. 內部類中為了保持對外部類的訪問性,會自動新增指向外部類例項的欄位。
  3. 在Java語言中欄位是無法過載的,兩個欄位的資料型別,修飾符不管是否相同,都必須使用不一樣的名稱,但是對於位元組碼來講,如果兩個欄位的描述符不一致,那欄位重名就是合法的.

4.7 方法表

欄位表後就是方法表了。

4.7.1 方法表計數器

前面兩個位元組依然用來表示方法表的容量,看下demo位元組碼中的值:
13.位元組碼-方法表容量計數器.png
其值為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位元組碼中的值:
14.位元組碼-方法表1.png
這是第一個方法表,我們來解讀一下這裡面的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屬性。

先跳過屬性表,我們再來看下第二個方法:
16.位元組碼-方法表2.png
訪問標誌的值為0x0001,查詢上面欄位訪問標誌的表格,可得欄位為public;

方法名索引的值為0x000b,查詢常量池中的第11項,可得:

  #11 = Utf8               add

描述符索引的值為0x000c,查詢常量池中的第12項,可得:

  #12 = Utf8               ()I

屬性計數器的值為0x0001,即這個方法表有一個屬性。
屬性名稱索引的值同樣也是0x0009,即這是一個Code屬性。
可以看到,第二個方法表就是我們自定義的add()方法了。

4.7.5 注意事項

  1. 如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現父類的方法。
  2. 編譯器可能會自動新增方法,最典型的便是類構造方法(靜態構造方法)<client>方法和預設例項構造方法<init>方法。
  3. 在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屬性:
15.位元組碼-Code屬性表1.png
屬性名索引的值為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虛擬機器位元組碼指令表,可以通過指令表來查詢指令的含義。

  1. 2a 指令,查表可得指令為aload_0,其含義為:將第0個Slot中為reference型別的本地變數推送到運算元棧頂。
  2. b7 指令,查表可得指令為invokespecial,其含義為:將運算元棧頂的reference型別的資料所指向的物件作為方法接受者,呼叫此物件的例項構造器方法、private方法或者它的父類的方法。其後面緊跟著的2個位元組即指向其具體要呼叫的方法。
  3. 00 01,指向常量池中的第1項,查詢上面的常量池可得:#1 = Methodref #4.#15 // java/lang/Object."<init>":()V 。即這是要呼叫預設構造方法<init>
  4. 2a 指令,同第1個。
  5. 04 指令,查表可得指令為iconst_1,其含義為:將int型常量值1推送至棧頂。
  6. b5 指令,查表可得指令為putfield,其含義為:為指定的類的例項域賦值。其後的2個位元組為要賦值的例項。
  7. 00 02,指向常量池中的第2項,查詢上面的常量池可得:#2 = Fieldref #3.#16 // com/april/test/Demo.num:I。即這裡要將num這個欄位賦值為1。
  8. 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屬性:
17.位元組碼-Code屬性表2.png
屬性名索引的值同樣為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屬性,先看第一個:
18.位元組碼-LineNumberTable屬性1.png
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屬性為:
19.位元組碼-LineNumberTable屬性2.png
這裡就不逐一看了,同樣使用javap命令可得:

      LineNumberTable:
        line 11: 0
        line 12: 10

所以這些行號是有什麼用呢?當程式丟擲異常時,我們就可以看到報錯的行號了,這利於我們debug;使用斷點時,也是根據原始碼的行號來設定的。

4.8.3.2 SourceFile屬性

前面將常量池、欄位集合、方法集合等都解讀完了。最終剩下的就是一些附加屬性了。
先來看看剩餘還未解讀的位元組碼:
18.位元組碼-附加屬性.png
同樣,前面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.總結

通過手動去解讀位元組碼檔案,終於大概瞭解到其構成和原理了。斷斷續續寫了比較長的時間,終於寫完了,撒花~

實際上,我們可以使用各種工具來幫我們去解讀位元組碼檔案,