一篇帶給你 Java Class 詳解
- 基于棧和基于寄存器指令區(qū)別?
- 什么是直接引用和間接引用?
- class文件怎么來的?
- apt與AMS字節(jié)碼插樁?
第一節(jié) Class 文件介紹
1、 背景
“計算機只認識0和1,所以我們寫的程序需要被編譯 器翻譯成由0和1構成的二進制格式才能被計算機執(zhí)行?!笔嗄赀^去了,今天的計算機仍然只能識別0和1,但由于最近十年內虛擬機以及大量建立在虛擬機之上的程序語言如雨后春筍般出現并蓬勃發(fā)展,把我們編寫的程序編譯成二進制本地機器碼(Native Code)已不再是唯一的選擇,越來越多的程序語言選擇了與操作系統(tǒng)和機器指令集無關的、平臺中立的格式作為程序編譯后的存儲格式。
Java 語言之所以能實現一次編譯到處運行,就是因為使用所有平臺都支持的字節(jié)碼格式
第二節(jié) Class類文件的結構
1、class文件格式
一個class文件是由下圖描述出來的。我們可以按這張表的格式去解釋一個class文件。
以u1、u2、u4、u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
接下來我們用這一小段樣本代碼來說明class文件的具體內容。再復雜的java源文件都是可以通過這樣的方式分析出來。
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
我們將上面的代碼用編譯器進行編譯得到一個TestClass.class文件。通過Windows工具“010Editor”對這個class文件進行閱讀。
下面是010Editor上面class二進制內容:
0A FE BA BE : 魔數(它的唯一作用是確定這個文件是否為一個能被虛擬機接受的Class文件)。
00 00 00 34 : 次版本號與主版本號 次版本號為0,主版本號為52(只能被jdk1.1~1.8 識別)。
class主版本與jdk版本關系(部分)。
2、 常量池
00 16 : 常量池數量 22,索引是1-21。
為什么常量池的索引不從0開始?
如果后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義,可以把索引值設置為0來表示。
0A 00 04 00 12( 常量索引:1):
0A: -> 10 通過查表 表示一個Methodref_info。
04: 找到索引為4的常量 -> java/lang/Object。
12 轉十進制得到18 , 這里找到常量池里18的常量代表 ()V。
得到結果: java/lang/Object () V。
09 00 03 00 13( 常量索引:2):
09: -> 09 表示一個Fieldref_info。
最終得到:com/havefun/javaapitest/TestClass 和 m i。
07 00 14( 常量索引:3):
最終結果 :com/havefun/javaapitest/TestClass。
07 00 15( 常量索引:4):
07 表示類信息。
15-> 21 是在常量的索引 -> java/lang/Object。
01 00 01 6D( 常量索引:5): m。
01 00 01 49( 常量索引:6): I。
01 00 06 3C 69 6E 69 74 3E( 常量索引:7):
01 00 03 28 29 56( 常量索引:8): ()V。
01 00 04 43 6F 64 65( 常量索引:9): Code。
01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65( 常量索引:10): LineNumberTable。
01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65( 常量索引:11):
LocalVariableTable
01 00 04 74 68 69 73( 常量索引:12): ----> this
01 00 23 4C 63 6F 6D 2F 68 61 76 65 66 75 6E 2F 6A 61 \
76 61 61 70 6974 65 73 74 2F 54 65 73 74 43 6C 61 73 73 3B( 常量索引:13):
Lcom/havefun/javaapitest/TestClass;
01 00 03 69 6E 63( 常量索引:14): inc
01 00 03 28 29 49( 常量索引:15): ()I
01 00 0A 53 6F 75 72 63 65 46 69 6C 65( 常量索引:16): SourceFile
01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61( 常量索引:17): TestClass.java
0C 00 07 00 08( 常量索引:18):
0C 表示字段或方法的部分引用。
07 ->
05 -> ()V
最終得到: // “”: ()V。
0C 00 05 00 06( 常量索引:19): 最終得到: // m:I
01 00 21 63 6F 6D 2F 68 61 76 65 66 75 6E 2F 6A 61 \
76 61 61 70 69 74 95 73 74 2F 54 65 73 76 43 6C 61 73 73( 常量索引:20):
最終得到:com/havefun/javaapitest/TestClass。
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74( 常量索引:21):
最終得到:java/lang/Object。
Javap -v 生成的內容,通過上面的分析就很容易看懂這個反編譯過后的常量池要表達的內容了!
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/havefun/javaapitest/TestClass.m:I
#3 = Class #20 // com/havefun/javaapitest/TestClass
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/havefun/javaapitest/TestClass;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/havefun/javaapitest/TestClass
#21 = Utf8 java/lang/Object
**訪問標識符、類索引、父類索引與接口索引集合 **。
下圖是class文件結構表里面的一部分,描述了訪問標識,類索引,父類索引與接口集合等。
00 21: ACC_PUBLIC | ACC_SUPER
下面是截取的常量池部分內容,類索引和父類索引都能在上面找到。
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/havefun/javaapitest/TestClass.m:I
#3 = Class #20 // com/havefun/javaapitest/TestClass
#4 = Class #21 // java/lang/Object
00 03: 類索引-> 常量池索引3
00 04: 父類索引-> 常量池索引4
00 00: 接口數量0
3、 字段信息
字段表結構如下:
字段訪問標志:
字段表信息:
00 01: 字段數量 1
通過字段表結構讀取6個字節(jié):00 02 00 05 00 06 00 00
00 02 訪問描述符:代表了private
00 05 字段名稱在常量池的索引:m
00 06 描述符在常量池的索引:I
00 00 屬性數量為0
結合起來字段就很容易知道這個是 private 的int類型的字段m。
4、方法表信息
繼續(xù)讀class文件后面的內容:00 02 表示有兩個方法。
方法表的結構:
向后讀方法表第一個方法:
00 01: 代表public方法 00 07:方法名 00 08:方法簽名()V
上面這小部分可以得到如下信息:
public com.havefun.javaapitest.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
00 01: 表示屬性表有一個屬性
屬性表結構:
00 09 00 00 00 2F: 通過常量池09表示Code(Code 的含義是Java代碼編譯成字節(jié)碼的指令), 后面4個字節(jié)表示接下來的屬性長度,2F轉十進制等于47。
Code對應的結構:
接下來的字節(jié)碼是:00 01 00 01 表示操作數棧最大深度為1;max_locals代表了局部變量表所需的存儲空間。
再接下來4個字節(jié):00 00 00 05(表示代碼長度)。
再向后讀5個字節(jié)表示代碼:2A B7 00 01 B1;。
- 2A:對應指令aload_0。是將第0個變量槽中為reference類型的本地變量推送到操作數棧頂。
- B7:指令為invokespecial。指令的作用是以棧頂的reference類型的數據所指向的對象作為方法接收者,調用此對象的實例構造器方法、private方法或者它的父類的方法。這個方法有一個u2類型的參數說明具體調用哪一個方法,它指向常量池中的一個CONSTANT_Methodref_info類型常量,即此方法的符號引用。
這里 00 01 也就是代表了常量池里面#1號常量 =>(// java/lang/Object.“”: ()V)這是一個構造方法。
因為Java默認在每個方法插入一個默認參數this,并且放在變量槽0的位置。上面兩條指令可以理解為 this = new Object(); 把這個this給實例化了。
- B1:對應指令為return。
說明:這里一個字節(jié)表示一條指令操作,那么也就說明Java虛擬機最多不會超過256條指令;
00 00 :異常表長度為0。
00 02:屬性列表數量為2。
那么上面可以得到如下信息:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
(1) 屬性表信息
00 0A 00 00 00 06 00 01 00 00 00 03:
通過查表0A對應的常量池里面的:LineNumberTable;LineNumberTable屬性用于描述Java源碼行號與字節(jié)碼行號(字節(jié)碼的偏移量)之間的對應關系。00 00 00 06 表示屬性長度為6個字節(jié);00 01表示有一個line_number_table;00 00表示是字節(jié)碼行號,00 03表示是Java源碼行號.
LineNumberTable對應的結構:
那么這可以得到如下信息:
LineNumberTable:
line 3: 0
00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00:
通過常量池得到0B代表的是 LocalVariableTable。
LocalVariableTable的屬性結構:
local_variable_info結構。
屬性長度0C轉十進制為12;00 01局部變量表長度為1。
00 00 00 05:表示start_pc和length屬性分別代表了這個局部變量的生命周期開始的字節(jié)碼偏移量及其作用范圍覆蓋的長度,兩者結合起來就是這個局部變量在字節(jié)碼之中的作用域范圍。
0C:在常量池查詢是表示 this;0D:是這個變量的描述符對應的:Lcom/havefun/javaapitest/TestClass。
最后的00 00表示:index是這個局部變量在棧幀的局部變量表中變量槽的位置。
通過上面這一小節(jié)可以得到如下信息:
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/havefun/javaapitest/TestClass;
5、 屬性信息
SourceFile屬性結構。
00 10:對應常量池的SourceFile 00 00 00 02:對應的屬性長度為2。
作用:如果不生成這項屬性,當拋出異常時,堆棧中將不會顯示出錯代碼所屬的文件名。
11:轉十進制得到17,sourcefile_index數據項是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源碼文件的文件名。通過常量池得知17對應常量為:TestClass.java。
第三節(jié) 基于棧指令簡介
1、 基于棧的解釋器執(zhí)行過程
以一段代碼作為例子說明演示字節(jié)碼執(zhí)行過程。
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
編譯成字節(jié)碼指令如下:
public int calc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 100 // 將100 推到操作數棧
2: istore_1 // 將操作數棧頂的整型值出棧并存放到第1個局部變量槽中
3: sipush 200 // 將200 推到操作數棧
6: istore_2 // 將操作數棧頂的整型值出棧并存放到第2個局部變量槽中
7: sipush 300 // 將300 推到操作數棧
10: istore_3 // 將操作數棧頂的整型值出棧并存放到第3個局部變量槽中
11: iload_1 // 將局部變量槽1的變量放入操作數棧
12: iload_2 // 將局部變量槽2的變量放入操作數棧
13: iadd // 將操作數棧中頭兩個棧頂元素出棧,做整型加法,然后把結果重新入棧
14: iload_3 // 將局部變量槽3的變量放入操作數棧
15: imul // 將操作數棧中頭兩個棧頂元素出棧,做整型乘法,然后把結果重新入棧
16: ireturn // 將結束方法執(zhí)行并將操作數棧頂 的整型值返回給該方法的調用者
2、 基于棧與基于寄存器指令集區(qū)別?
以同樣的1+1這個計算來進行舉例。
基于棧的指令集如下:
iconst_1
iconst_1
iadd
istore_0
基于寄存器指令集如下:
mov eax, 1
add eax, 1
這兩種指令集的優(yōu)勢與劣勢:
- 基于棧的指令集主要優(yōu)點是可移植。
- 基于寄存器的指令會比基于棧的指令少,但是每條指令會邊長。
- 基于棧指令集的主要缺點是理論上執(zhí)行速度相對來說會稍慢一些。
個人總結
class文件的結構分析就到這里了,通過一個簡單的類去探索編譯器如何實現類的編寫,那么再復雜的類我們也能一步一步分析出來,只是需要我們更加細心。我們了解了這些文件的生成過程,個人認為有如下好處:
- 知道javap -v 反編譯class文件的輸出內容到底是怎么來的。
- class文件怎么描述一個Java方法或者一個變量。運用方向比如字節(jié)碼增強,動態(tài)修改或者生成等都是能夠實現的。