1. 程式人生 > >jvm 類文件結構學習

jvm 類文件結構學習

屬於 空間 先生 long acc res 以及 test tag

本文以代碼示例來學習 java 類文件的結構,其中對類文件結構的學習均來自周誌明先生所著的 《深入理解 Java 虛擬機》一書,在此表示誠摯的感謝。

代碼如下:

 1 package com.reycg.jvm;
 2 
 3 public class ReferenceCountingGC {
 4     
 5     public Object instance = null;
 6     
 7     public static void testGC() {
 8         ReferenceCountingGC objA = new ReferenceCountingGC();
9 ReferenceCountingGC objB = new ReferenceCountingGC(); 10 11 objA.instance = objB; 12 objB.instance = objA; 13 14 objA = null; 15 objB = null; 16 17 System.gc(); 18 } 19 20 21 public static void main(String[] args) {
22 ReferenceCountingGC.testGC(); 23 } 24 25 }

使用 winHex 打開對應的 class 文件,如下圖顯示

技術分享

下面對這些數據進行分析

Magic Number 魔數

技術分享

每個 Class 文件的頭 4 個字節稱為 magic number,它的作用就是確定這個文件是否是一個能夠被 jvm 接受的 Class 文件。

0xCAFEBABE 咖啡寶貝,這個充滿魔性的代號就是 class 文件的 magic number。

Class 文件的版本

技術分享

緊接著 magic number 的四個字節就是 Class 文件的版本號,其中前兩個字節表示次版本號,後兩個字節表示主版本號。

00 00 00 33 就表示 JDK 1.7.0 版本。

常量池

技術分享

上圖中發灰的部分,也就是緊緊挨著主次版本號後的區域就是常量池的入口。

常量池可以理解為 Class 文件中的資源倉庫。它在 class 文件中一般占用的空間最大。

常量池入口出放置了一個 u2 類型的數據,u2 也就是兩個字節的意思,表示常量池容量的計數值。

技術分享

從圖中可看出數值為 0x0023,從 Data interpreter 中可看出十進制為 35。

與 Java 習慣語言不一致的是,這個容量計數是從 1 開始,而不是從 0 開始的,因此這就表示常量池中包含 34 個常量。

常量池中主要存放兩大類常量:字面量 Literal 和 符號引用 Symbolic References。其中

  • 字面量比較接近 Java 語言中的常量概念,包括
    • 文本字符串
    • 聲明為 final 的常量值等
  • 符號引用則屬於編譯原理方面的概念
    • 類和接口的全限定名
    • 字段的名稱和描述符
    • 方法的名稱和描述符  

class 文件中不會保存各個方法,字段的最終的內存信息,而是在 jvm 加載 class 文件的時候才會進行動態連接,翻譯成具體的內存地址。

常量池的每一項常量都是一個表,在 JDK 1.7 中共有 14 種結構各不相同的結構數據。在 《深入理解 java 虛擬機》一書中有詳細的介紹,此處不再贅述。

需要說明的是每個類型結構數據表結構,最開始都是一個 u1 類型的標誌位 tag,可以根據這個 tag 直接對應到到底是哪個常量類型。

技術分享

觀察上圖, 07 則表示 tag 標誌位,通過它則可知道這個常量屬於 CONSTANT_Class_info 類型,該類型表示一個類或者接口的符號引用。

CONSTANT_Class_info 的結構如下

類型  名稱 數量
u1 tag 1
u2 name_index 1

該結構的 name_index 是一個索引值,指向常量池中一個 Constant_Utf8_info 類型常量,該常量則代表了這個類或者接口的全限定名稱。

此處 name_index 則為 0x0002 也就是指向常量池中的第 2 項常量。

技術分享

繼續查找第二項常量,如上圖,所有的灰色區域都是第二項常量的範圍。

從 tag 為 01 上可得到該常量為 CONSTANT_Utf8_info 型常量。該常量描述為 UTF-8 編碼的字符串。

該常量的結構為

類型 名稱 數量
u1   tag 1
u2 length 1
u1     bytes length

length 值則表示這個 UTF-8 編碼的字符串長度是多少個字節。它後面會緊跟著長度為 length 字節的連續數據,該段連續數據是一個使用 UTF-8 縮略編碼表示的字符串。

由於 length 值是 u2 類型的數據,因此它能表示的最大長度為 65535,因此字符串的最大長度也就是 65535 (64k),如果 Java 中定義了超過 64k 的英文字符的變量或者方法名,就會無法編譯。

截止到現在,我們已經分析了常量池中 34 個常量中的兩個,其余的 32 個常量都可以通過類似的方法計算出來。後面的常量方式也都可以通過這種方式計算出來,此處就不再一一分析。

實際上有現成的軟件可以幫助我們完成這一過程, Oracle 公司專門有一個用於分析 Class 文件字節碼的工具 javap。關於 javap 的 help 文檔如下所示

技術分享

下圖列出了使用 javap 工具的 -verbose 參數輸出的 ReferenceCountingGc.class 文件字節碼內容,註意此處省略了常量池之外的其他信息。

技術分享

訪問標誌

技術分享

在常量池結束後,後面的兩個字節就表示訪問標誌 access_flags, 這個標誌用來標識一些 class 或者接口層次的訪問信息。

通過頁首的 java 代碼可以看出 ReferenceCountingGC 的訪問標誌為 public class,其中

  • public 標誌名稱為 ACC_PUBLIC, 值為 0x0001
  • ACC_SUPER 標誌在 JDK 編譯出來後就必須為真,其值為 0x0020

其他的標誌都為假,因此它的 access_flags 為 0x0001|0x0020=0x0021. 從上圖看出確實為 0x0021

類索引,父類索引與接口索引集合

Class 文件由類索引,父類索引和接口索引,這 3 項數據來確定該 class 的繼承關系,其中

  • this_class 類索引
    • 確定這個類的全限定名
    • u2 類型
  • super_class 父類索引
    • u2 類型
    • 確定這個類的父類的全限定名
  • interface 接口索引集
    • 是一組 u2 類型的數據集合
    • implements 語句對應,從左至右依次排列

技術分享

訪問標誌後的 3個 u2 數據依次為 0x0001, 0x0003, 0x0000。也就是說

  • 類索引 0x0001
  • 父類索引 0x0003
  • 接口索引的大小 0x0000

通過查詢 javap 命令計算出的常量池,找出對應的 class 和父 class 的常量

技術分享

從圖中對應得出

  • 類索引指向 #1 // com/reycg/jvm/ReferenceCountingGC
  • 父類索引 #3 //// java/lang/Object

字段表集合 field_info

字段表用來描述接口或者 class 中聲明的變量。

字段 field 則包括

  • 類級變量
  • 實例級變量

此處需要總結下在 Java 中描述一個字段需要包括的信息

  • 作用域 (public, private, protected 修飾符)
  • 實例變量還是類變量 static 修飾符
  • 可變性 final
  • 並發可見性 volatile 修飾符,也就是是否強制從主內存讀寫
  • 是否可以被序列化 transient 修飾符
  • 字段類型數據(基本類型,對象,數組)
  • 字段名稱

根據上面的分析,下表就列出了字段表的最終格式

類型 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 description_index 1
u2 attribute_count 1
attribute_info attributes attributes_count

access_flags 就是訪問修飾符,有些是強制的,有些不是強制的,依據具體情況而定。

name_index 和 description_index,都是對常量池的引用,但兩者有所區別

  • name_index 表示字段的簡單名稱
  • description_index 表示字段和方法的描述符

簡單名稱,描述符以及限定名這 3 個字符串的概念

  • 全限定名,如 com/reycg/jvm/ReferenceCountingGC
  • 簡單名稱,指沒有類型和參數修飾的方法或者字段名稱,比如 testGC
  • 方法和字段的描述符
    • 作用是用來描述字段的數據類型,方法的參數列表和返回值
    • 方法的參數列表中包含
      • 數量
      • 類型
      • 順序

描述符標識含義如下表所示

標識字符 含義
B 基本類型 byte
C 基本類型char
D 基本類型 double

F

基本類型 float
I 基本類型 int

J

基本類型 long
S 基本類型 long
Z 基本類型 boolean
V 特殊類型 void
L 對象類型 如 Ljava/lang/object

對於數組來說,每個維度都會使用一個前置的 "[" 字符來描述,如一維數組 java.lang.String[] 將會被記錄為 "[java/lang/String"。

用描述符來描述方法時,會按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號內,如

  • void inc() ---> ()V
  • java.lang.String toString() --> ()Ljava/lang/String
  • int indexOf(String str, int fromIndex) --> indexOf:(Ljava/lang/String;)I

技術分享

現在結合前面字段表的最終格式表格,繼續看 winhex class 數據

  • 紅色框中 u2 類型數據 0x0001 表示字段表的數量 field count ,說明這個 class 只有一個字段表數據
  • 後面會緊跟著對這個字段表數據進行描述
    • 0x0001 表示 access_flags public
    • 0x0005 表示 name_index ,從前面的常量表中可查到 #5 是一個 utf8 字符串 instance
    • 0x0006 表示 description_index, 指向常量表中的 #6 表示 L/java/lang/object
    • 0x0000 表示 attributes_count 其值為 0, 那麽後面也就不需要 attribute_info 字段了

技術分享

由此可以推斷出源代碼定義字段為

private Object instance;

方法表集合

如果對字段表集合能夠做到很好的理解,要理解方法表集合也就非常容易了。方法表的結構與字段表的結構非常類似,如下表所示。

類型 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

字段表集合和方法表集合最大的區別在於訪問標誌和屬性表集合的可選項有所區別。

技術分享

繼續分析 class 文件

  • 0x0003 表示集合中有兩個方法
  • 第一個方法集合數據
    • access_flags 0x0001 : 方法為 public
    • name_index 0x0007 : 指向常量表 #7 = Utf8 <init>
    • descriptor_index 0x0008 #8 = Utf8 ()V
    • attributes_count 0x0001 表示該方法的屬性表集合中有一項屬性
    • attributes 屬性名稱索引 0x0009 對應常量是 Code,說明該屬性是方法的字節碼描述

有上面分析,第一個方法集合描述的是 <init> 方法,它是由編譯器自動添加的方法,叫做實例構造器方法,會在編譯器優化章節中涉及到。

屬性表集合

屬性表這個概念在前面已經出現了數次,字段表,class 文件,方法表都可以攜帶自己的屬性表集合,用來描述某些場景下專有的信息。

一個符合負責的屬性表應該滿足下表中所定義的結構

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info 1

在 java 虛擬機規範 Java SE 7 版中,預定義的屬性有 21 項,下文會結合著 class 字節碼對其中一些屬性的關鍵部分進行講解。

1. Code 屬性

Java 程序方法體中的代碼經過 Javac 編譯器處理後,最終會變成字節碼指令存儲在 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_table_length
u2 attribute_count 1
attribute_info attributes attributes_count

結合下圖 class 文件內容進行分析

技術分享

上面講到,方法 <init> 的 attribute 是 Code, 0x0009。

attribute_length 表示屬性值的長度 0x0000003c 轉換成十進制就是 60 字節

max_stack 0x0002 表示操作數棧 Operand Stacks 深度的最大值。在方法執行的任意時刻,操作數棧都不會超過這個深度。

max_locals 0x0001 表示局部變量表所需要的存儲空間。max_locals 的單位是 Slot。

Slot 是虛擬機為局部變量分配內存使用的最小單位。對於 byte, char, float,int, short,boolean 和 returnAddress 等長度不超過 32 位的類型數據,每個局部變量占用一個 slot,而 double 和 long 這兩種 64 位的數據類型則需要兩個 Slot 來存放。

code_length 和 code 用來存儲 Java 源程序編譯後生成的字節碼指令。code_length 表示字節碼長度 0x0000000A,表示 10 個字節。後面的 10 個字節,也就是藍色方框中的數據就是 code 字節碼。

code 是用來存儲字節碼指令的一系列字節流,叫做字節碼指令。其中每個指令就是一個 u1 類型的單字節,當 jvm 讀取到 code 中的一個字節碼時,就會找到對應的字節碼代表什麽指令,並且可以知道該指令是否需要跟隨參數,以及參數應當如何理解。

剛才分析到了 init 方法的 code 字節碼,知道占10個字節,下面我們就操刀對這10個字節的 code進行操作分析

winhex class 數據如下

技術分享

這裏再補充一下,下面查詢的字節碼指令對應關系是從《深入理解 jvm 虛擬機》 附錄B 得出。

jvm 翻譯這些字節的過程如下

  1. 讀入 2A,查表得到對應的指令為 a_load0, 表示將第 0 個 slot 中為 reference 類型的本地變量推送到操作數棧
  2. 讀入 B7 ,對應指令 invokespecial 調用超類構造方法,實例初始化方法,私有方法。解釋下就是將剛才推送的 reference 類型的本地變量 作為方法參數,調用此對象的實例構造器方法。invokespecial 指令本身有一個 u2 類型的參數,來說明具體調用哪個方法
  3. 讀入 0x000A 就是 invokespecial 的參數 #10 從常量池中可以查到 #10 表示
    1. Methodref #3.#11 // java/lang/Object."<init>":()V
    2. 也就是實例構造器 <init> 方法的符號引用
  4. 讀入 2A,a_load0
  5. 讀入 01, aconst_null 將 null 推送到棧頂
  6. 讀入 B5,對應指令為 putfield 為指定的類的實例域賦值,putfield 後面跟一個 u2 的參數
  7. 讀入 00 0C,為 putfield 的參數,到常量表裏對應得到 #13
    1. #13 = NameAndType #5:#6 // instance:Ljava/lang/Object;
    2. 對應到代碼,就是 public Object instance = null;
  8. 讀入 B1 查表得到對應的指令為 return,含義是返回此方法,並且返回值為 void

由此當前整個方法結束。

現在來看下使用 javap 命令將 Class 文件中的 init 方法計算出來得到的結果。

技術分享

從圖中可看出,與我們的計算結果能夠精確的吻合在一起。

這裏有個問題 locals = 1, args_size =1 表示參數為 1, 但是在 init 方法體中並沒有定義任何局部變量和參數。這是為什麽呢?

這是因為在任何實例方法裏面,都可以通過 "this" 關鍵字訪問到此方法所屬的對象。 javac 編譯器在編譯的時候,會把對 this 關鍵字的訪問轉變為對一個普通方法參數的訪問,然後再 jvm 調用此參數時自動傳入此參數。

而如果方法聲明為 static 類型就不會將 this 傳入,看代碼中的 testGC 就可以得到驗證。

技術分享

未完待續。。。

jvm 類文件結構學習