在class文件結(jié)構(gòu)解析一文中,我們介紹了class文件的構(gòu)成床绪,整個class文件一共包含3部分共16個屬性:
- 3個描述文件屬性的數(shù)據(jù)項:魔數(shù)和主次版本號
- 11個描述類屬性的數(shù)據(jù)項:類、字段歧蕉、方法等信息
- 2個描述代碼屬性的數(shù)據(jù)項:屬性表,描述方法體內(nèi)的具體內(nèi)容
其中文件屬性和類屬性在上一篇中已經(jīng)有過介紹,本文將主要介紹一下屬性表珠洗。在最新的JVM規(guī)范中,一共定義了21個屬性若专,接下來我將對其中一些關(guān)鍵屬性進行分析许蓖。
對于每一個屬性,它的結(jié)構(gòu)可以分為3部分:
- 一個u2類型的屬性名稱(attribute_name_index):從常量池中引用的一個常量
- 一個u4類型的屬性長度(attribute_length):屬性值所占用的字節(jié)數(shù)
- attribute_length個u1類型的屬性值:具體的屬性值
1. Code屬性
java源文件方法體中的代碼經(jīng)過編譯后调衰,最終存儲在Code屬性內(nèi)膊爪,它的結(jié)構(gòu)如下:
類型 | 名稱 | 數(shù)量 |
---|---|---|
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表示屬性值的長度
- max_stack表示操作數(shù)棧的最大深度嚎莉,jvm運行時會根據(jù)這個值來分配棧幀中的操作數(shù)棧深度
- max_locals表示局部變量表所需要的存儲空間米酬,單位為slot
- code_length代表字節(jié)碼指令長度
- code代表具體的字節(jié)碼指令,根據(jù)jvm規(guī)范趋箩,每個字節(jié)碼指令占用一個字節(jié)赃额,jvm可以自動識別該指令是否需要接收參數(shù)。
- exception_table_length表示異常表占用的字節(jié)數(shù)
- exception_table表示具體的異常表
- Code屬性本身還有自己的一些屬性表叫确,包括LineNumberTable跳芳、LocalVariableTable和StackMapTable,這些屬性不是必須的竹勉,如果有的話飞盆,會在attributes_count和attributes中體現(xiàn)出來
2. LineNumberTable屬性
LineNumberTable是Code屬性中的一個子屬性,用來描述java源文件行號與字節(jié)碼文件偏移量之間的對應關(guān)系次乓。當程序運行拋出異常時吓歇,異常堆棧中顯示出錯的行號就是根據(jù)這個對應關(guān)系來顯示的,它的結(jié)構(gòu)如下:
類型 | 名稱 | 數(shù)量 |
---|---|---|
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 |
其中l(wèi)ine_number_info結(jié)構(gòu)如下:
類型 | 名稱 | 數(shù)量 | 含義 |
---|---|---|---|
u2 | start_pc | 1 | 字節(jié)碼偏移量 |
u2 | line_number | 1 | java源文件行號 |
3. LocalVariableTable屬性
LocalVariableTable也是Code屬性中的一個子屬性檬输,用來描述棧幀的局部變量表中變量與java源碼中變量的對應關(guān)系照瘾,其結(jié)構(gòu)如下:
類型 | 名稱 | 數(shù)量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | llocal_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
其中l(wèi)ocal_variable_info結(jié)構(gòu)如下:
類型 | 名稱 | 數(shù)量 | 含義 |
---|---|---|---|
u2 | start_pc | 1 | 變量生命周期開始時的字節(jié)碼偏移量 |
u2 | length | 1 | 變量作用范圍覆蓋的字節(jié)數(shù) |
u2 | name_index | 1 | 索引值,指向變量名稱 |
u2 | descriptor_index | 1 | 索引值丧慈,指向變量描述符 |
u2 | index | 1 | 變量在棧幀中slot的位置 |
LineNumberTable和LocalVariableTable都不是運行時必須的析命,可以在javac中使用-g:none選項來取消生成這兩個屬性,取消前后反編譯出來的文件將丟失這兩個屬性逃默,如下圖所示:
4. 實例分析
我們還是繼續(xù)以上一篇中的代碼為例進行分析:
java源文件:
利用javap得到的字節(jié)碼內(nèi)容(這里只給出了main方法的):
// 這一部分是描述方法的元數(shù)據(jù)完域,在上一篇已經(jīng)分析過
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
// 這一部分描述方法體的具體內(nèi)容
Code:
// 操作數(shù)棧最大深度為3软吐,局部變量表最多需要一個slot,有一個入?yún)? // 如果是實例方法吟税,還會增加一個this對象的引用作為隱形參數(shù)
stack=3, locals=1, args_size=1
// 具體的字節(jié)碼指令凹耙,共占用28個字節(jié)
// 獲取System.out屬性值姿现,壓入棧頂
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 創(chuàng)建StringBuilder實例,并將其引用壓入棧頂
3: new #3 // class java/lang/StringBuilder
// 復制棧頂元素肖抱,并壓入棧頂
6: dup
// 調(diào)用init()方法备典,彈出棧頂元素
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
// 加載常量Hello,壓入棧頂
10: ldc #5 // String Hello
// 調(diào)用StringBuilder的append()方法
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 獲取name屬性值
15: getstatic #7 // Field name:Ljava/lang/String;
// 調(diào)用StringBuilder的append()方法
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 調(diào)用StringBuilder的toString()方法
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
// 調(diào)用PrintStream的println()方法
24: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
// java源碼行號與字節(jié)碼文件偏移量的對應關(guān)系
LineNumberTable:
line 14: 0
line 15: 27
// 棧幀中局部變量表變量與java源碼變量的對應關(guān)系
LocalVariableTable:
Start Length Slot Name Signature
0 28 0 args [Ljava/lang/String;
再來看看main方法code屬性對應的十六進制文件意述,按照code屬性結(jié)構(gòu)對其進行分析提佣,可以發(fā)現(xiàn)其內(nèi)容與javap得到的結(jié)果完全一致。
main方法里只看到了加載常量Hello的操作荤崇,那么有人可能會問拌屏,靜態(tài)常量name的屬性值是在哪里加載的呢?實際上术荤,這一步在cinit()方法中就完成了倚喂。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
// 類方法,不會傳入this引用瓣戚,故args_size=0
stack=1, locals=0, args_size=0
// 從常量池加載靜態(tài)常量务唐,壓入棧頂
0: ldc #10 // String JVM
// 為靜態(tài)屬性name賦值
2: putstatic #7 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
通過查看方法的字節(jié)碼,可以更直觀的看出方法內(nèi)部的執(zhí)行邏輯带兜。比如說對于字符串連接操作:
"Hello " + name;
底層實際上是通過新建一個StringBuilder對象來實現(xiàn)的:
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(name);
sb.toString();
事實上,很多問題都可以由此迎刃而解吨灭,比如說i++和++i到底有什么區(qū)別刚照。以下4段代碼,最終i的值分別是多少呢喧兄?稍有經(jīng)驗的程序員都可以輕松給出答案无畔,但是其實現(xiàn)原理是什么呢,我們不妨從字節(jié)碼角度來略探一二吠冤。
public static void incr1() {
int i = 0;
i = i++;
}
public static void incr2() {
int i = 0;
i++;
}
public static void incr3() {
int i = 0;
i = ++i;
}
public static void incr4() {
int i = 0;
++i;
}
public static void incr1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: iinc 0, 1
6: istore_0
7: return
LineNumberTable:
line 20: 0
line 21: 2
line 22: 7
LocalVariableTable:
Start Length Slot Name Signature
2 6 0 i I
public static void incr2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: return
LineNumberTable:
line 26: 0
line 27: 2
line 28: 5
LocalVariableTable:
Start Length Slot Name Signature
2 4 0 i I
public static void incr3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: iload_0
6: istore_0
7: return
LineNumberTable:
line 32: 0
line 33: 2
line 34: 7
LocalVariableTable:
Start Length Slot Name Signature
2 6 0 i I
public static void incr4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: return
LineNumberTable:
line 38: 0
line 39: 2
line 40: 5
LocalVariableTable:
Start Length Slot Name Signature
2 4 0 i I
具體分析可以參考占小狼的文章從字節(jié)碼角度分析 i++ 和 ++i 實現(xiàn)浑彰,圖文并茂,清晰易懂拯辙。