寫在前面
讀懂 Class 文件是了解虛擬機運行原理的重要步驟,本文將結合 《深入理解Java虛擬機》中的內容,和大家分享解讀 Class 文件的過程。
一、什么是 Class 文件
定義
Class 文件是一組以 8 位字節為基礎單位的二進制流
簡單地說,Class 文件是由一堆二進制組成的。這些二進制的排列是有一定規則的,虛擬機就是根據這些規則把數據加載到內存中使用的。
實例
我們知道 Class 文件的生成可以由 Java 編譯而來。接下來是一個簡單的例子:
- 首先準備一個 java 文件,里面包含一個屬性和方法:
package com.sky.test;
public class Test {
private int i = 10;
private void test(){
}
}
- ide 編譯一下生成 Class 文件,這里用的是 AndroidStudio。早就知道編譯后會生成 Class 文件,最后果然在 build 文件夾下找到了 Test.class:
CommonBaseApp\test\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\sky\test
找到文件后使用 Sublime Text(一個文字編輯器)打開,需要注意的是這里該 Class 文件是用十六進制的格式打開的,里面代表的是十六進制的值:
好家伙,這一堆字符。但是不要慌,前面提到過 Class 文件是遵循一定規則的,熟悉了這個規則很容易就看懂了。
二、Class 文件的結構
一張比較經典的圖描述了 Class 文件的結構:
這里的字節實際上是 Java 虛擬機規定的數據類型,1 個字節即 U1、兩個字節即 U2 類型。 下文的字節描述對應 U* 類型。
三、Class 文件解析
接下來我們試著用剛才的文件,試著匹配下這個結構圖。
Magic Number
魔數:Test.class 開頭四個字節 cafe babe
叫做 Magic Number。加上這么一個開頭是出于安全性考慮,也是虛擬機識別 Class 文件的重要標識。
Version 版本
后面的四個字節為 0000 0033
。這部分前兩個字節 0x0000
表示次版本號為 0,主版本號 0x0033
轉換為十進制是 51,說明當前文件版本為 51。
這個版本的 Class 文件可以被 JDK 1.7.0 版本以上虛擬機執行,如果用版本不兼容的虛擬機加載 Class 文件會報錯。
Constant Pool 常量池
再往后面的 0x0015
表示常量池元素的數量,0x0015
轉換為十進制為 21,說明該常量池有 20 個元素。
這里的下標是從 1 開始的,所以是 20 個元素而不是 21 個。把開始下標 0 置空是為了表達某些情況下 “不引用任何一個常量池項目” 的含義。
#1 常量池第一個元素
接下來就是常量池里面的元素了,緊接著的值是 0x0a=10
。
我們可以把常量池中的元素看作是一個整體串,其中有 tag 表示其類型、index 表示其指向的常量池下標、length 表示其長度等等。這么一個個整體組成了常量池表。
那么這個 0x0a=10
代表的什么意思呢,看下面的表:
去找 tag 為 10 的常量,是 CONSTANT_Methodref_info,說明這個元素記錄的是一個方法的信息。
[圖片上傳失敗...(image-f0f77-1600151856238)]
說明接下來的數據是一個方法引用的信息,把這個 CONSTANT_Methodref_info 看作是一個類,看這個類的構成:
- U1(一個字節)的 tag 標記,也就是
0a=10
。 - U2(兩個字節)的指向該方法的類在常量池中的索引值,也就是這個方法屬于哪個類,并指出這個類在常量池中的位置。
在字節碼中是0x0004=4
說明屬于常量池 #4 的元素所表示的類。 - U2(兩個字節)的指向其名稱及類描述符的索引值,也就是這個方法的名字以及返回值所在的位置。
在字節碼中是0x0011=17
說明屬于常量池 #17 的元素所表示的信息。
#2 常量池第二個元素
下一個元素的 tag 為 0x09=9
。
從表中可知為 CONSTANT_Fieldref_info,即字段引用信息。
它也包含三種信息;
- U1(一個字節) 的 tag 標記,也就是
0x09=9
; - U2(兩個字節)的指向該字段所屬類或接口的常量池索引,也就是這個字段所屬類或接口在常量池中的位置。
在字節碼中為0x0003=3
說明屬于常量池下標為 3 的元素所表示的類。 - U2(兩個字節)的指向該字段描述符的常量池索引,也就是這個字段的名稱和描述符在常量池中的位置。
在字節碼中為0x0012=18
說明指向常量池下標為 18 的元素所表示的信息。
#F 利用工具查看字節碼
其實整個 Class 字節碼可以通過工具查看,我們直接借用工具來看。IDE 中安裝 jclasslib Bytecode viewer,安裝重啟。選中要查看字節碼的類,點這個:
如果點擊提示未找到 class 文件,說明需要編譯一下。編譯之后再次點擊出現如下信息:
這樣這個 Class 文件的所有信息就展示出來了,開始是總覽信息,包含 Java 版本、類名、常量池數量等信息。前面我們已經分析過了,這個工具也是根據規則去讀取 Class 文件的。
第二個元素的詳細信息
前面我們已經知道第二個元素所屬類的索引是 3,點到常量池第三個選項可以看到如下信息:
可以看到工具告訴我們第三個元素是 CONSTANT_Class_info 也就是類信息,并且右邊的 Class name 指向了 cp_info #19,也就是常量池的 #19 表示了第二個元素所屬的類。可以從工具看到 #19 表示的字符串是 com/sky/test/Test,說明第二個元素屬于該類。
回頭看第二個元素的字段描述符所在的索引,18。我們就不再去字節碼挨個找第 18 個元素了,直接借助工具看:
第 18 個元素是 CONSTANT_NameAndType_info 類型的,說明是名稱和類型的描述:
- tag:一個字節的 tag,倒推一下是
0x0c=12
; - 兩個字節的方法名稱索引:
0x0005=5
。 - 兩個字節的方法描述符索引:
0x0006=6
。
如果去字節碼去找的話是能找到一串連續的 0c 0005 0006
的,說明倒推的數據也是正確的。
到這里簡單捋一捋,第二個元素的方法名和類型指向了 #18,而 #18 的信息又指向了 #5 和 #6。就是說搞清楚 #5 和 #6 就搞清楚第二個元素到底是什么了。
#5 #6
借助工具可知 #5 #6 的類型為 CONSTANT_Utf8_info 也就是一個字符串,字符串是怎么描述的呢,需要再看之前的表:
- tag:一個字節的 tag
0x01=1
表示當前信息是字符串; - length:兩個字節的信息表示當前字符串的長度;
- bytes:一個字節的信息表示當前字符串的 ASCII 碼。
字節碼中找到第 5 個元素如下圖:
0x01
表示字符串,0x0001
表示長度為 1。0x69=69
表示字符串的值,而 69 在 ASCII 碼中表示字母 i。
千辛萬苦終于找到了個字符串 i,可以猜測是我們之前 java 代碼里定義的 int 變量 i。
接著再找字節碼的第 6 個元素 01000149
,0x01
代表字符串、0x0001
表示字符串長度為 1、0x49=73
在 ASCII 碼中表示大寫字母 I。
所以 #5 和 #6 表示的屬性類型為 I 說明這是一個 int 類型的值,并且屬性名為 i。
#19 的名字
看懂了字符串怎么表示,#19 這個表示類名的字符串也可以看懂了。無非就是 0x01 開頭的,后面跟了長度以及一堆跟 ASCII 碼對應的數值。這里就不再逐一分析了,直接看答案就好:
描述了 Test 類的全限定名,也就是能表示 Test 類唯一性的路徑以及類名。
訪問標志
常量池結束后,緊接著兩個字節代表訪問標志,這個標志用于識別這個 Class 是類還是接口、是 public 還是 abstract。如果是類,是否為 final 等。
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為Public |
ACC_PRIVATE | 0x0002 | 是否為Private |
ACC_PROTECTED | 0x0004 | 是否為Protected |
ACC_STATIC | 0x0008 | 是否為static |
ACC_FINAL | 0x0010 | 是否被聲明為final,只有類可以設置 |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial字節碼指令的新語義.jdk 1.0.2 以后編譯出來的都必須為真. |
ACC_INTERFACE | 0x0200 | 標志這是一個接口 |
ACC_ABSTRACT | 0x0400 | 是否為abstract類型,對于接口或者抽象類來說,次標志值為真,其他類型為假 |
ACC_SYNTHETIC | 0x1000 | 標志這個類并非由用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 標志這是一個注解 |
ACC_ENUM | 0x4000 | 標志這是一個枚舉 |
我們知道 Test 是一個類,并且是高版本 jdk 編譯出來的,所以它的 flag 應該是 0x0001
和 0x0020
。把他們進行或運算,結果為 0x0021
。無論是從字節碼還是用工具看,都能印證這個結果。
類索引
類索引最終表示的是該類的類名。它指向的是常量池中一個 CONSTANT_Class_info 類型的常量,而該常量又指向表示其名稱的字符串常量。
圖中標紅的位置就是類索引在常量池中的下標,前文分析字段的時候已經知道 #3 為一個 CONSTANT_Class_info 類型,指向表名其名稱的字符串為
com/sky/test/Test
正是該類的全限定名。
父類索引
父類索引緊隨著類索引,字節碼中是 0x0004 指向的是 #4。與類索引相同,最終表示為 java/lang/Object
。說明 Test 類的父類為 Object。
接口索引
接口索引在父類索引之后,它的表示為一組 u2 類型的數據的集合。字節碼中為 0x0000
說明沒有實現接口。如果有接口實現,則先表示數量,然后再指向接口全限定名在常量池中的下標。
假如 Test 實現了兩個接口,并且兩個接口在常量池中的表示位置為 3 和 4,那么對應的字節碼應該是 0002 0003 0004
。
字段表集合
字段表用于描述接口或者類中聲明的變量。
緊接著的字節碼表示字段表,字段表的結構如下:
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attributes_info | attributes | attributes_count |
[圖片上傳失敗...(image-688c7b-1600151856238)]
- 0x0001:該 Class 只有一個字段;
- 0x0002:該字段的訪問標志(access_flags),也就是 private,可以參照上文的訪問標志表;
- 0x0005:字段名(name_index),常量池索引 #5,也就是 i;
- 0x0006:字段描述符(descriptor_index),常量池索引 #6,也就是 I。說明是 int 類型的數據。
- 0x0000:屬性表(attributes_info)為 0,說明不需要額外的信息描述該變量。如果該變量為
final sttic int i = 12
,則可能會存在一個名稱為 ConstantVaule 的屬性,并指向常量 12。
方法表集合
方法表的表現形式與字段表的描述幾乎完全一致,方法表結構:
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attributes_info | attributes | attributes_count |
接下來就是方法表的內容了,看字節碼:
開頭的 0x0002
:表示有兩個方法,一個是由編譯器生成的方法,一個是我們寫的 test 方法。
第一個方法
- 0x0001:access_flag,表示該方法為 public;
- 0x0007:方法名常量池下標,執行 #7,方法名為 "< init >";
- 0x0008:方法描述符的索引,對應常量為 "()V" 表示返回 void;
- 0x0001:該方法有一個屬性表;
這里涉及到屬性表,上文不止一次提到過屬性表。在 Class 文件、字段表、方法表都可以攜帶自己的屬性表集合,用于描述某些場景的專有信息。
我們接著看后面的字節碼:
- 0x0009:該方法屬性表的屬性索引,對應常量為 "Code";
- 0x0000 0039:該方法屬性表的長度;
- 再往后就是 Code 屬性的詳細描述。
Code 是虛擬機預定義的屬性之一,表示接下來是一段由編譯器生成的字節碼,也就是虛擬機可執行的代碼。
Code 也有自己的結構,接下來的數據組成為一個完整的 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 | attributes_count | 1 |
attribute-info | attributes | attributes_count |
attribute_name_index("Code")和 attribute_length("0x0000 0039"
)已經分析過,接下來根據這個表繼續對字節碼進行分析。
- 0x0002:max_stack = 2,操作數棧最大深度,這里是 2。在方法執行的任何時刻,操作數棧都不會超過這個深度。
- 0x0001:max_locals = 1,局部變量表所需的存儲空間。
局部變量表的單位是 Slot,對于 byte、char、float、int、short、boolean 和 returnAddress 等長度不超過 32 位的數據類型,每個局部變量占用 1 個 Slot。double 和 long 這兩種 64 位的數據類型需要兩個 Slot 來存放。 - 0x0000 000b:code_length = 11,說明接下來 11 個字節都存放的是字節碼操作指令。
接下來的字節碼指令串為 2a b7 00 01 2a 10 0a b5 00 02b1
,每個字節都代表了一種字節碼指令。
- 2a:aload_0 從局部變量0中裝載引用類型值入棧,這里是裝入 this;
- b7:invokespecial 調用操作數棧頂對象的實例構造器方法、private 方法或者它父類的方法,這里調用的是父類 Object 的方法。
這個方法有兩個字節說明具體調用的哪一個方法,緊隨其后的是 0x00 01 是常量池索引 #1 的 <init> 方法。 - 2a:aload_0 從局部變量0中裝載引用類型值入棧;
- 10:valuebyte值帶符號擴展成int值入棧。
- 0a:1(long)值入棧。
- b5:給對象字段賦值。兩個字節的參數表示給哪個對象賦值,也就是后面的 0x0002,查常量池是為參數 i 賦值。
- b1:返回 void。
字節碼指令本文不再深入,繼續看后面的字節碼:
- 0x0000 :exception_table_length = 0,說明該方法不存在異常信息表;
- 0x0002 :attributes_count=2,說明 Code 屬性表內部還有兩個表。
接下來的內容是第一個表 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 |
- 0x000a:第一個表名,常量池 #10 為 LineNumberTable,這個表用于描述 Java 源碼行號與字節碼(偏移量)之間的對應關系。
- 0x0000000a:屬性長度為 10;
- 0x0002:LineNumberTable 表長度為 2;
再往后面記錄的是 LineNumberTable 中 line_number_info 的信息。line_number_info 是由兩個 u2 類型數據組成的,前面 u1 表示字節碼行號、后面 u1 表示代碼行號。
我們知道 LineNumberTable 的長度為 2,line_number_info 應該有兩對 u2 數據:
- 0x0000:start_pc 字節碼行號 0;
- 0x0009:line_number 行號 9;
- 0x0004:start_pc 字節碼行號 4;
- 0x000b:line_number 行號 11;
再往后是第二個表 "LocalVariableTable",這個表用于描述棧幀中局部變量表中的變量與 Java 源碼中定義的變量直接的關系:
- 0x000b:常量池 #11,指向 “LocalVariableTable” 字符串;
- 0x0000000c:屬性長度為 12;
- 0x0001:局部變量信息表長度為 1;
- 0x0000 : start_pc = 0 該局部變量字節碼偏移量;
- 0x000b:length = 11,該變量作用的長度 ;
- 0x000c : name_index= "this",局部變量名稱;
- 0x000d : descriptor_index #13 ("Lcom/sky/test/Test"),局部變量描述符;
- 0000 index=0,局部變量在棧幀局部變量表 Slot 的位置。
到此第一個方法以及描述完畢了,這個方法大概就是調用了父類 Object 的 init 方法,用來初始化變量等一系列操作。
鑒于篇幅第二個方法本文就不再分析,猜想也知是 Test.class 中的 test 方法,這塊內容有一個了解即可。
Attribute
再往后就是 Attribute 了,Attribute 包含 "SourceFile" 表示 Class 文件的名稱。通過工具查詢是 #16 變量,名稱為 Test.java。
總結
以上就是本文的全部內容了,感謝閱讀。