瞭解Class文件的結構組成,對於我們後續的JVM以及Java原理深入學習是很有幫助的,因爲Class文件幫我們默默的做了很多事,比如、爲什麼對象方法中可以直接使用this變量?!本文將帶領大家,一步步,從開頭到結尾,逐字逐句分析、瞭解、深入Class文件組成和結構!
本文類容比較多,不建議直接看,建議收藏,並跟着我的案例一起做!如果10個人中能有3個人跟着學完本文(敲完代碼),那麼本人的目的就算達到了。
1 Class文件與無關性
Java虛擬機不和包括Java在內的任何語言綁定,它只與“Class文件”這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集和符號表以及若干其他輔助信息。各種不同平臺的虛擬機與所有平臺都統一使用的程序存儲格式——字節碼(ByteCode,Class文件語法)是構成平臺無關性的基石,也是實現語言無關性的基石。
2 Class文件的結構
2.1 Class文件結構概述
任何一個Class文件都對應着唯一一個類或接口的定義信息,但反過來說,類或接口並不一定都得定義在文件裏(譬如類或接口也可以通過類加載器直接生成)。Class文件是一組以8位字節爲基礎單位的二進制流。
Java 虛擬機規範規定 Class 文件格式採用一種類似與 C 語言結構體的僞結構體來存儲數據,這種僞結構體中只有兩種數據類型:無符號數和表。
- 無符號數屬於基本的數據類型,以 u1、u2、u4、u8來分別代表 1 個字節、2 個字節、4 個字節和 8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼結構構成的字符串值。對於字符串,則使用u1數組進行表示。
- 表是由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣性地以「_info」結尾。表用於描述有層次關係的複合結構的數據,整個 Class 文件就是一張ClassFile 表,它由下表中所示的數據項構成。
根據 Java 虛擬機規範,一個Class文件由單個 ClassFile 結構組成:
ClassFile {
u4 magic; //Class 文件的標誌,魔術
u2 minor_version; //Class 的附版本號
u2 major_version; //Class 的主版本號
u2 constant_pool_count; //常量池表項的數量
cp_info constant_pool[constant_pool_count-1]; //常量池表項,索引爲1~constant_pool_count-1
u2 access_flags; //Class 的訪問標誌(類訪問修飾符)
u2 this_class; //表示當前類的引用
u2 super_class; //表示父類的引用
u2 interfaces_count; //實現接口數量
u2 interfaces[interfaces_count]; //接口索引數組
u2 fields_count; //此類的字段表中的字段數量
field_info fields[fields_count]; //一個類會可以有多個字段,字段表
u2 methods_count; //此類的方法表中的方法數量
method_info methods[methods_count]; //一個類可以有個多個方法,方法表
u2 attributes_count; //此類的屬性表中的屬性數量
attribute_info attributes[attributes_count]; //屬性表集合
}
Class文件字節碼結構組織示意圖:
下面來一個一個具體介紹
2.2 魔數
u4 magic; //Class 文件的標誌
每個Class文件的頭4個字節稱爲魔數(Magic Number),它的唯一作用是確定這個文件是否爲一個能被虛擬機接受的Class文件。之所以使用魔數而不是文件後綴名來進行識別主要是基於安全性的考慮,因爲文件後綴名是可以隨意更改的(當然魔術也可以改,只不過比起改後綴名來說更復雜)。
Class 文件的魔數值固定爲「0xCAFEBABE」。Java一直以咖啡爲代言,CAFEBABE可以認爲是 Cafe Babe,讀音上和Cafe Baby很近。所以這個也許就是代表Cafe Baby的意思。
2.2.1 案例
Java源碼
public class ClassFile {
public static final String J = "2222222";
private int k;
public int getK() {
return k;
}
public void setK(int k) throws Exception {
try {
this.k = k;
} catch (IllegalStateException e) {
e.printStackTrace();
} finally {
}
}
public static void main(String[] args) {
}
}
運行後,會出現 Class文件,拿到Class文件,使用notepad++編輯器打開–點擊插件–HEX-Editor(沒有該插件的自行下載)–view in HEX,即可以16進制形式查看Clsaa文件。
當然也可以直接使用HEX-Editor軟件打開:hex-editor。
我們先看前四個字節:
我們可以看到魔數在首位,並且正是0xcafebabe。
2.3 補充:各種名以及描述符
2.3.1 全限定名和非限定名
Class文件中的類和接口,都是使用全限定名,又被稱作Class的二進制名稱。例如“com/ikang/JVM/classfile”是這個類的全限定名,僅僅是把類全名中的“.”替換成了“/”而已,爲了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個“;”表示全限定名結束。
非限定名又被稱作簡單名稱,Class文件中的方法、字段、局部變量、形參名稱,都是使用簡單名稱,沒有類型和參數修飾,例如這個類中的getK()方法和k字段的簡單名稱分別是“getK”和“m”。
非限定名不得包含ASCII字符. ; [ / ,此外方法名稱除了特殊方法名稱和方法之外,它們不能包含ASCII字符<或>,字段名稱或接口方法名稱可以是或,但是沒有方法調用指令可以引用,只有invokespecial指令可以引用。
2.3.2 描述符
描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值類型。
2.3.2.1 字段描述符
根據描述符規則基本數據類型(byte、char、doubIc、float, int、loog、shon, boolean)以及代表無返回伯的void類型都用一個大寫字符來表示, 而對象類型則用字符L加對象的全限定名來表示,數組則用[
字段描述符的類型含義表
字段描述符 | 類型 | 含義 |
B | byte | 基本類型byte |
C | char | 基本類型char |
D | double | 基本類型double |
F | float | 基本類型float |
I | int | 基本類型int |
J | long | 基本類型long |
LClassName; | reference | 對象類型,例如Ljava/lang/Object; |
S | short | 基本類型short |
Z | boolean | 基本類型boolean |
[ | reference | 數組類型 |
對象類型的實例變量的字段描述符是L+類的二進制名稱的內部形式。
對於數組類型,每一維度將使用一個前置的“[”字符來描述,如一個定義爲“java.lang.String[][]”類型的二維數組,將被記錄爲:“[[Ljava/lang/String;”,一個整型數組“int[]”將被記錄爲“[I”
2.3.2.2 方法描述符
它基於描述符標識字符含義表所示的字符串的類型表示方法, 同時對方法簽名的表示做了一些規定。它將函數的參數類型寫在一對小括號中, 並在括號右側給出方法的返回值。
比如, 若有如下方法:
Object m(int i, double d, Thread t) {… }
則它的方法描述符爲:
(IDLjava/lang/Thread;)Ljava/lang/Object;
可以看到, 方法的參數統一列在一對小括號中, “I”表示int , “D”表示double,“Ljava/lang/Thread;”表示Thread對象。小括號右側的Ljava/lang/Object;表示方法的返同值爲Object對象類型。
2.4 Class文件版本
u2 minor_version; //Class 的附版本號
u2 major_version; //Class 的主版本號
緊接着魔數的 4 個字節存儲的是 Class 文件的版本號:第 5 和第 6 兩個字節是附版本號(Minor Version),第 7 和第 8 個字節是主版本號(Major Version)。高版本的 JDK 能夠向下兼容低版本的 Class 文件,但虛擬機會拒絕執行超過其版本號的 Class 文件(低版本不能向上打開高版本文件)。
JDK主版本號 | Class主版本號 | 16進制 |
1.1 | 45.0 | 00 00 00 2D |
1.2 | 46.0 | 00 00 00 2E |
1.3 | 47.0 | 00 00 00 2F |
1.4 | 48.0 | 00 00 00 30 |
1.5 | 49.0 | 00 00 00 31 |
1.6 | 50.0 | 00 00 00 32 |
1.7 | 51.0 | 00 00 00 33 |
1.8 | 52.0 | 00 00 00 34 |
2.4.1 案例
使用上面的案例,向後取四個字節:
我們發現主版本號爲0x0034,轉換爲十進制爲52,可知屬於JDK1.8
2.5 常量池
u2 constant_pool_count; //常量池表項的數量
cp_info constant_pool[constant_pool_count-1]; //常量池表項,索引爲1~constant_pool_count-1
緊接着主次版本號之後的是常量池的常量數量constant_pool_count,但是常量池的常量實際數量是 constant_pool_count-1(常量池計數器是從1開始計數的,將第0項常量空出來是有特殊考慮的,索引值爲0代表“不引用任何一個常量池項;其它集合類型,包括接口索引集合、字段表集合、方法表集合等容量計數都是從 0 開始。”)。
之後就是常量池的實際內容constant_pool,每一項以類型、長度、內容或者、類型、內容的格式依次排列存放。
常量池是 Class 文件結構中與其他項目關聯最多的數據類型,也是佔用 Class 文件空間最大的數據項目之一,同是它還是 Class 文件中第一個出現的表類型數據項目。
java虛擬機指令並不依賴類、接口、類實例或者數組的運行時佈局。相反,指令依靠常量池中的符號信息,常量池是整個Class文件的基石。
Class常量池主要存放兩大常量:字面量和符號引用。
- 字面量比較接近於 Java 語言層面的的常量概念,如文本字符串、聲明爲 final 的常量值等。
- 符號引用則屬於編譯原理方面的概念。包括下面三類常量:類和接口的全限定名;字段的名稱和描述符;方法的名稱和描述符
補充:由於Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量來描述名稱,所以CONSTANT_Utf8_info型常量的最大長度也就是Java中方法、字段名的最大長度。而這裏的最大長度就是length的最大值,既u2類型能表達的最大值65535。所以Java程序中如果定義了超過64KB英文字符的變量或方法名,將會無法編譯。
2.5.1 常量池表項目類型
在JDK1.8中有14種常量池項目類型,每一種項目都有特定的表結構,這14種表有一個共同的特點:開始的第一位是一個 u1 類型的標誌位 -tag 來標識常量的類型,代表當前這個常量屬於哪種常量類型。
常量池tag類型表:
常量類型 | 標誌(tag) | 描述 |
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_MothodType_info | 16 | 標誌方法類型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
2.5.2 常量池表結構
每種常量類型均有自己的表結構,非常繁瑣:
常量池項目結構表:
常量 | 描述 | 項目 | 類型 | 項目描述 |
CONSTANT_Utf8_info | UTF-8編碼的字符串 | tag | u1 | 值爲1 |
length | u2 | UTF-8編碼的字符串佔用的字節數 | ||
bytes[length] | u1 | 長度爲length的UTF-8編碼的字符串 | ||
CONSTANT_Integer_info | 整型字面量 | tag | u1 | 值爲3 |
bytes | u4 | 按照高位在前存儲的int值 | ||
CONSTANT_Float_info | 浮點型字面量 | tag | u1 | 值爲4 |
bytes | u4 | 按照高位在前存儲的float值 | ||
CONSTANT_Long_info | 長整型字面量 | tag | u1 | 值爲5 |
bytes | u8 | 按照高位在前存儲的long值 | ||
CONSTANT_Double_info | 雙精度浮點型字面量 | tag | u1 | 值爲6 |
bytes | u8 | 按照高位在前存儲的double值 | ||
CONSTANT_Class_info | 類或接口的符號引用 | tag | u1 | 值爲7 |
name_index | u2 | 指向全限定名常量項的索引 | ||
CONSTANT_String_info | 字符串類型字面量 | tag | u1 | 值爲8 |
string_index | u2 | 指向字符串字面量的索引 | ||
CONSTANT_Fieldref_info | 字段的符號引用 | tag | u1 | 值爲9 |
class_index | u2 | 指向聲明字段的類或者接口描述符CONSTANT_Class_info的索引項 | ||
name_and_type_index | u2 | 指向字段描述符CONSTANT_NameAndType的索引項 | ||
CONSTANT_Methodref_info | 類中方法的符號引用 | tag | u1 | 值爲10 |
class_index | u2 | 指向聲明方法的類描述符CONSTANT_Class_info的索引項 | ||
name_and_type_index | u2 | 指向名稱及類型描述符CONSTANT_NameAndType的索引項 | ||
CONSTANT_InterfaceMethodref_info | 接口中方法的符號引用 | tag | u1 | 值爲11 |
class_index | u2 | 指向聲明方法的接口描述符CONSTANT_Class_info的索引項 | ||
name_and_type_index | u2 | 指向名稱及類型描述符CONSTANT_NameAndType的索引項 | ||
CONSTANT_NameAndType_info | 字段或方法的部分符號引用 | tag | u1 | 值爲12 |
name_index | u2 | 指向該字段或方法名稱常量項的索引 | ||
descriptor_index | u2 | 指向該字段或方法描述符常量項的索引 | ||
CONSTANT_MethodHandle_info | 表示方法句柄 | tag | u1 | 值爲15 |
reference_kind | u1 | 值必須在1~9範圍,它決定了方法句柄的類型。方法句柄類型的值表示方法句柄的字節碼行爲 | ||
reference_index | u2 | 值必須是對常量池的有效索引 | ||
CONSTANT_MethodType_info | 標識方法類型 | tag | u1 | 值爲16 |
descriptor_index | u2 | 值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示方法的描述符 | ||
CONSTANT_InvokeDynamic_info | 表示一個動態方法調用點 | tag | u1 | 值爲18 |
bootstrap_method_attr_index | u2 | 值必須是對當前Class文件中引導方法表的bootstrap_methods[]數組的的有效索引 | ||
name_and_type_index | u2 | 值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 |
當然,所有的常量池條目都有如下的通用結構如下:
cp_info {
u1 tag;
u1 info[];
}
2.5.3 案例
2.5.3.1 constant_pool_count
繼續向後走,到了constant_pool_count,取兩個字節:
可以看到常量池的表項目數量0x2f,轉換爲十進制爲47,那麼常量池表項爲47-1=46項。因爲常量池計數器是從1開始計數的,將第0項常量空出來是有特殊考慮的,索引值爲0代表不引用任何一個常量池項;但是其它集合類型,包括接口索引集合、字段表集合、方法表集合等容量計數都是從 0 開始。
2.5.3.2 第一項常量
繼續向後走,到了constant_pool了,常量類型的通用第一項均是u1長度的tag,那麼向後取一個字節,即取第一個常量的tag:
該值爲0x0a,轉換爲10進制就是tag=10,查找上面的常量池項目類型表,可知第一個常量屬於CONSTANT_Methodref_info,表示類中方法的符號引用,在常量池結構表中查找它的結構,如下:
由於後兩個字段都是指向常量池的索引,因此完整結構爲:
繼續向後走4個字節:
class_index=0x0006,表示指向聲明方法的類描述符CONSTANT_Class_info的索引項由常量池第6個常量字符串指定。
name_and_type_index=0x0025,表示指向名稱及類型描述符CONSTANT_NameAndType的索引項由常量池第37個常量字符串指定。
2.5.3.3 第二項常量
第一項常量結束,繼續向下查找第二個常量項的tag。
該tag爲0x09,轉換爲十進制就是9,查找上面的項目類型表,可知第二個常量項屬於CONSTANT_Fieldref_info,表示字段的符號引用,在結構表中查找它的結構,如下:
可知後面兩個u2類型項目同樣都是指向常量池的索引,向後取四個字節:
class_index=0x0005,表示指向聲明字段的類或者接口描述符CONSTANT_Class_info的索引項由常量池第5個常量字符串指定。
name_and_type_index=0x0026,表示指向字段描述符CONSTANT_NameAndType的索引項由常量池第38個常量字符串指定。
2.5.3.4 第三項常量
第二項常量結束,繼續向下查找第三個常量項的tag:
該tag爲0x07,轉換爲十進制就是7,查找上面的項目類型表,可知第三個常量項屬於CONSTANT_Class_info,表示類或接口的符號引用,在結構表中查找它的結構,如下:
可知後面一個u2類型項目是指向常量池的索引,向後取兩個字節:
name_index=0x0027,表示該類的全限定類名由常量池第39個常量字符串指定。
2.5.3.5 第四項常量
第三項常量結束,繼續向下查找第四項常量的tag:
該tag值爲0x0a,轉換而爲10進制就是tag=10,查找上面的項目類型表,可知第四個常量屬於CONSTANT_Methodref_info,表示類中方法的符號引用,在結構表中查找它的結構,如下:
可知後面兩個u2類型項目都是索引,向後取四個字節:
class_index=0x0003,表示指向聲明方法的類描述符CONSTANT_Class_info的索引項由常量池第3個常量字符串指定。回過頭看我們上面找到的的第三個常量,剛好是CONSTANT_Class_info類型,說明我們到目前所有查找是正確的。
name_and_type_index=0x0028,表示指向名稱及類型描述符CONSTANT_NameAndType的索引項由常量池第40個常量字符串指定。
第四項常量結束,目前找到了4個,還剩下42個,這種方法看起來很麻煩,還容易出錯。我們可以使用javap工具加上-v參數幫我們分析字節碼文件。
2.5.4 javap命令幫助分析Class文件
javap是jdk自帶的反解析工具。它的作用就是根據class字節碼文件,反解析出當前類對應的code區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。
我們可以直接使用idea自帶的控制檯
找到class文件,右鍵——open in Terminal
輸入 javap -v ClassFile.class 指令,即可輸出Class文件信息。
信息如下:
Classfile /J:/Idea/jvm/target/classes/com/ikang/JVM/classfile/ClassFile.class
Last modified 2020-4-4; size 960 bytes
MD5 checksum a7fc5ccb0b193f1d32e2658e68fed475
Compiled from "ClassFile.java"
public class com.ikang.JVM.classfile.ClassFile
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#37 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I
#3 = Class #39 // java/lang/IllegalStateException
#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V
#5 = Class #41 // com/ikang/JVM/classfile/ClassFile
#6 = Class #42 // java/lang/Object
#7 = Utf8 J
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 ConstantValue
#10 = String #43 // 2222222
#11 = Utf8 k
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;
#20 = Utf8 getK
#21 = Utf8 ()I
#22 = Utf8 setK
#23 = Utf8 (I)V
#24 = Utf8 e
#25 = Utf8 Ljava/lang/IllegalStateException;
#26 = Utf8 StackMapTable
#27 = Class #39 // java/lang/IllegalStateException
#28 = Class #44 // java/lang/Throwable
#29 = Utf8 Exceptions
#30 = Class #45 // java/lang/Exception
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Utf8 args
#34 = Utf8 [Ljava/lang/String;
#35 = Utf8 SourceFile
#36 = Utf8 ClassFile.java
#37 = NameAndType #13:#14 // "<init>":()V
#38 = NameAndType #11:#12 // k:I
#39 = Utf8 java/lang/IllegalStateException
#40 = NameAndType #46:#14 // printStackTrace:()V
#41 = Utf8 com/ikang/JVM/classfile/ClassFile
#42 = Utf8 java/lang/Object
#43 = Utf8 2222222
#44 = Utf8 java/lang/Throwable
#45 = Utf8 java/lang/Exception
#46 = Utf8 printStackTrace
{
public static final java.lang.String J;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String 2222222
public com.ikang.JVM.classfile.ClassFile();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
public int getK();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field k:I
4: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
public void setK(int) throws java.lang.Exception;
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field k:I
5: goto 19
8: astore_2
9: aload_2
10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V
13: goto 19
16: astore_3
17: aload_3
18: athrow
19: return
Exception table:
from to target type
0 5 8 Class java/lang/IllegalStateException
0 5 16 any
8 13 16 any
LineNumberTable:
line 16: 0
line 20: 5
line 17: 8
line 18: 9
line 20: 13
line 19: 16
line 21: 19
LocalVariableTable:
Start Length Slot Name Signature
9 4 2 e Ljava/lang/IllegalStateException;
0 20 0 this Lcom/ikang/JVM/classfile/ClassFile;
0 20 1 k I
StackMapTable: number_of_entries = 3
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/IllegalStateException ]
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 2 /* same */
Exceptions:
throws java.lang.Exception
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 25: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
}
SourceFile: "ClassFile.java"
截取其中的常量池部分,也就是Constant pool部分:
Constant pool:
#1 = Methodref #6.#37 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I
#3 = Class #39 // java/lang/IllegalStateException
#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V
#5 = Class #41 // com/ikang/JVM/classfile/ClassFile
#6 = Class #42 // java/lang/Object
#7 = Utf8 J
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 ConstantValue
#10 = String #43 // 2222222
#11 = Utf8 k
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;
#20 = Utf8 getK
#21 = Utf8 ()I
#22 = Utf8 setK
#23 = Utf8 (I)V
#24 = Utf8 e
#25 = Utf8 Ljava/lang/IllegalStateException;
#26 = Utf8 StackMapTable
#27 = Class #39 // java/lang/IllegalStateException
#28 = Class #44 // java/lang/Throwable
#29 = Utf8 Exceptions
#30 = Class #45 // java/lang/Exception
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Utf8 args
#34 = Utf8 [Ljava/lang/String;
#35 = Utf8 SourceFile
#36 = Utf8 ClassFile.java
#37 = NameAndType #13:#14 // "<init>":()V
#38 = NameAndType #11:#12 // k:I
#39 = Utf8 java/lang/IllegalStateException
#40 = NameAndType #46:#14 // printStackTrace:()V
#41 = Utf8 com/ikang/JVM/classfile/ClassFile
#42 = Utf8 java/lang/Object
#43 = Utf8 2222222
#44 = Utf8 java/lang/Throwable
#45 = Utf8 java/lang/Exception
#46 = Utf8 printStackTrace
{
我們發現,這樣能更加直觀的查看Class的信息,左邊表示第一個常量,右邊是指向的常量索引或者具體的常量值,我們分析前四項:
從上面的數據可以看出,我們在前面把前四項的常量都計算了出來,並且與我們的就算結果一致。後面的分析我們會繼續和javap的結果做對比。
查看右邊是具體的常量值,會發現其中有一部分常量是咱們沒見過的,比如"< init > " : ( ) V 、LineNumberTable、LocalVariableTable等常量。實際上它們會被後面的字段表、方法表、屬性表所使用到,他們用來描述無法使用固定字節表達的內容,比如描述方法的返回值、有幾個參數、每個參數類型等。因爲Java中的 “類” 是無窮無盡的, 無法通過簡單的無符號字節來描述一個方法用到了什麼類, 因此在描述方法的這些信息時, 需要引用常量表中的符號引用進行表達。
2.6 Class訪問標記(access_flag)
u2 access_flags; //Class的訪問標記(類訪問修飾符)
在常量池結束之後,緊接着的兩個字節代表訪問標誌,這個標誌用於識別一些類或者接口層次的訪問權限和屬性,例如:這個 Class 是類還是接口,是否爲 public 或者 abstract 類型,如果是類的話是否聲明爲 final 等等。
類訪問和屬性修飾符標誌表
表示名稱 | 值 | 含義 |
ACC_PUBLIC | 0x0001 | 是否爲public類型 |
ACC_FINAL | 0x0010 | 是否被聲明爲final類型,只有類可設置 |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial字節碼指令的新語意,invokespecial指令的語意在JDB1.2之後發生過改變,爲了區別這條指令使用哪種語意,JDK1.0.2之後編譯出來的類的這個標誌都必須爲真。 |
ACC_INTERFACE | 0x0200 | 標識這是一個接口 |
ACC_ABSTRACT | 0x0400 | 是否爲abstract類型,對於接口或者抽象類來說,此標誌爲真,其它類值爲假 |
ACC_SYNTHETIC | 0x1000 | 標識這個類並非由用戶代碼產生的 |
ACC_ANNOTATION | 0x2000 | 標識這是一個註解 |
ACC_ENUM | 0x4000 | 標識這是一個枚舉 |
2.6.1 案例
我們使用javap的幫助,可以快速找到常量池的每一位常量項佔用的字節,這樣就能快速找到常量池後面的訪問標誌。通過javap可知,最後一項常量是printStackTrace,那麼我們可以輕易找到他所在的字節,如下圖:
然後嘗試計算access_flag的值,該測試類爲public,因此ACC_PUBLIC爲真,加上使用的是JDK1.8編譯,那麼ACC_SUPER一定爲真,其他標誌則爲假。綜合起來access_flag應該爲:0x0001+0x0020=0x0021。
下一項u2就是access_flag,向後取兩個字節來進行驗證:
發現確實是0x0021,說明咱們計算正確。
2.7 當前類索引、父類索引、接口索引集合
u2 this_class; //表示當前類的引用
u2 super_class; //表示父類的引用
u2 interfaces_count; //接口數量
u2 interfaces[interfaces_count]; //接口索引集合
類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名,由於 Java 語言的單繼承,所以父類索引只有一個,除了 java.lang.Object 之外,所有的 java 類都有父類,因此除了 java.lang.Object 外,所有 Java 類的父類索引都不爲 0。他們各自指向常量池中一個CONSTANT_Class_info類型的類描述符常量,通過CONSTANT_Class_info的索引可以定位到內部的CONSTANT_utf8_info常量中的全限定類名字符串。
接口索引數組用來描述這個類實現了哪些接口,這些被實現的接口將按implents(如果這個類本身是接口的話則是extends) 後的接口順序從左到右排列在接口索引集合中。如果該類沒有實現任何接口,則interfaces_count爲0,並且後面的interfaces集合不佔用字節。
2.7.1 案例
2.7.1.1 當前類和父類索引
繼續向後四個字節:
我們找到這兩個u2類型的引用,分別是0x0005和0x0006,他們的十進制值爲5和6,表示指向常量池中的第5和6個常量項。
使用javap,查看查看常量池中的第5和6個常量驗證:
確實是CONSTANT_Class_info類型,繼續查看CONSTANT_Class_info的索引指向的第41和42個常量:
確實是CONSTANT_utf8_info字符串類型,後面就能找到本類和父類的全限定類名了,說明我們計算正確。
2.7.1.2 接口個數、索引集合
向後走兩位,查看實現的接口個數interfaces_count信息:
interfaces_count接口計數爲0x0000,即表示不繼承接口,那麼後面的interfaces集合就字段不佔用字節了。
到此,當前類索引、父類索引、接口索引集合,結束。
2.8 字段表集合(field_info)
u2 fields_count; //此類的字段表中的字段數量
field_info fields[fields_count]; //一個類會可以有多個字段,字段表
字段表(field info)用於描述接口或類中聲明的變量。字段包括類級變量以及實例變量,但不包括在方法內部聲明的局部變量。不會列出從父類或者父接口繼承來的字段,但有可能列出原本Java代碼沒有的字段,比如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。
field info(字段表) 的結構如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}access_flags: 字段的作用域(public,private,protected修飾符),是實例變量還是類變量(static修飾符),可否被序列化(transient修飾符),可變性(final),可見性(volatile 修飾符,是否強制從主內存讀寫)。 只有一個
name_index:對常量池的引用,CONSTANT_utf8_info,表示的字段的名稱;只有一個
descriptor_index: 對常量池的引用,CONSTANT_utf8_info,表示字段和方法的描述符;只有一個
attributes_count: 一個字段還會擁有一些額外的屬性,表示attributes_count 存放屬性的個數;只有一個
attributes[attributes_count]: 存放具體屬性具體內容集合。有attributes_count個
在Java語言中字段是無法重載的,兩個字段的數據類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於字節碼來講,如果兩個字段的描述符不一致,那字段重名就是合法的。
額外的屬性表示一個字段還可能擁有一些屬性, 用於存儲更多的額外信息, 比如初始化值、一些註釋信息等。屬性個數存放在attributes_count 中, 屬性具體內容存放於attributes數組。常量數據比如“public static finall”類型的字段,就有一個ConstantValue的額外屬性。
字段訪問和屬性標識表:
標誌名稱 | 標誌值 | 含義 |
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_TRANSIENT | 0x0080 | 字段爲transient |
ACC_SYNTHETIC | 0x1000 | 字段由編譯器自動產生的 |
ACC_ENUM | 0x4000 | 字段是enum |
attribute屬性結構(在屬性表部分會深入講解):
以常量屬性爲例,常量屬性的結構爲:
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
attribute_name_index 爲2 字節整數, 指向常量池的CONSTANT_Utf8_info類型, 並且這個字符串爲“ ConstantValue" 。該屬性是所有類型的字段都有的。
attribute_length由4個字節組成, 表示這個屬性的剩餘長度爲多少。對常量屬性而言, 這個值恆定爲2,表示從Ox00000002 之後的兩個字節爲屬性的全部內容。該屬性是所有類型的字段都有的。
constantvalue_ index 表示屬性值,但值並不直接出現在屬性中,而是指向常量池引用,即存放在常量池中,不同類型的屬性會和不同的常量池類型匹配。
2.8.1 案例
2.8.1.1 fields_count
向後走兩個字節,查看fields_count:
0x0002,即有兩個字段,可以猜測是常量J和普通對象變量k。
2.8.1.2 第一個字段
下面來看第一個字段,先取前八個字節:
八個字節,兩個一組,表示字段作用域、對常量池的引用–字段的名稱、對常量池的引用–字段和方法的描述符、額外屬性。
access_flags表示字段作用域爲0x19,查詢字段訪問標誌表,可知,該字段作用域爲:ACC_PUBLIC+ACC_STATIC+ACC_FINAL,即public static final ,可以猜到該字段就是類中的“J”字段。
name_index表示對常量池的引用(字段的名稱),值分別0x0007,即第七個常量
descriptor_index表示對常量池的引用(字段和方法的描述符),0x0008,即第八個常量。
我們在 javap命令下的常量池中查找:
看到值爲“J”,剛好是我們類中常量的字段的名稱。字段和方法的描述符爲Ljava/lang/String;,即String類型,同樣符合我們的預期。
attributes_count表示額外屬性個數,值爲0x0001,即1個。那麼後面的第五個屬性就是描述該屬性的結構,由於是常量屬性,格式會佔用8個字節。
attributes描述額外屬性的具體內容集合,該例子中只有一個數據,如下8字節:
前兩個字節爲attribute_name_index,指向常量池,0x09即第九個常量,常量字段值固定爲“ConstantValue”,我們通過javap驗證:
發現確實如此。
後4個字節爲attribute_length**, 表示這個屬性的剩餘長度爲多少,常量固定爲2,從上面可以看到確實是2;表示從Ox00000002 之後的兩個字節爲屬性的全部內容。
最後取兩個字節表示constantvalue_ index,即屬性值引用,同樣指向常量池,0x0a即第10個常量,我們通過javap驗證:
第10項表示該常量是String類型,並且具體值又指向了第43個常量,我們找到它:
這裏就是存的具體值了,確實爲“2222222”,和代碼中一致。
2.8.1.3 第二個字段
下面來看第二個字段,同樣先看前四個屬性:
access_flag訪問標誌爲 0x0002,查詢字段access表,可知該字段爲ACC_PRIVATE,即private。
name_index字段名引用爲0x000b,即指向常量池第11個字段:
我們看到字段名確實是k。
descriptor_index字段描述符引用爲0x000c,即指向常量池第12個字段:
可以看到,由於是基本類型,並且是int,因此只有一個I,符合預測。
attributes_count表示額外的屬性,這裏是0x0000,即沒有額外屬性。那麼後面的額外屬性集合自然不佔用字節了。
到此字段表分析結束。
2.9 方法表集合(method_info)
u2 methods_count; //此類的方法表中的方法數量
method_info methods[methods_count]; //一個類可以有個多個方法,方法表
Class 文件存儲格式中對方法的描述與對字段的描述幾乎採用了完全一致的方式。方法表的結構如同字段表一樣,依次包括了訪問標記、名稱索引、描述符索引、屬性表集合幾項。而方法中的具體代碼則是存放在屬性表中了。類似於字段表,子類不會記錄父類未重寫的方法,同上編譯器可能自己加方法,比如< init > 、< cinit >。
method_info(方法表的) 結構如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flag表示訪問標記,因爲volatile修飾符和transient修飾符不可以修飾方法,所以方法表的訪問標誌中沒有這兩個對應的標誌,但是增加了synchronized、native、abstract等關鍵字修飾方法,所以也就多了這些關鍵字對應的標誌。access_flag 取值表如下:
標誌名稱 | 標誌值 | 含義 |
ACC_PUBLIC | 0x0001 | 方法是否爲public |
ACC_PRIVATE | 0x0002 | 方法是否爲private |
ACC_PROTECTED | 0x0004 | 方法是否爲protected |
ACC_STATIC | 0x0008 | 方法是否爲static |
ACC_FINAL | 0x0010 | 方法是否爲final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否爲synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由編譯器產生的橋接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定參數 |
ACC_NATIVE | 0x0100 | 方法是否爲native |
ACC_ABSTRACT | 0x0400 | 方法是否爲abstract |
ACC_STRICTFP | 0x0800 | 方法是否爲strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是由編譯器自動產生的 |
name_index 表示方法的名稱, 它是一個指向常量池的索引。
descriptor_ index爲方法描述符, 它也是指向常量池的索引, 是一個字符串, 用以表示方法的簽名(參數、返回值等)。關於方法描述符,在最開始已經介紹了。
和字段表類似, 方法也可以附帶若干個屬性, 用於描述一些額外信息, 比如方法字節碼等, attributes_count 表示該方法中屬性的數量, 緊接着, 就是attributes_count個屬性的描述。
attribute_info通用格式爲:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
其中, attribute_name_ index 表示當前attribute的名稱, attribute_length爲當前attribute的剩餘長度, 緊接着就是attribute_length個字節的byte 數組。常見的 attribute在屬性表會有詳細介紹。
2.9.1 案例
2.9.1.1 methods_count
接着上面的案例,先看methods_count
這說明有四個方法,那麼肯定有些方法是編譯時自動生成的
2.9.1.2 第一個方法
然後,看第一個方法的method_info的內部前四個字段:
access_flag爲0x0001,查找方的access_flag 取值表可知,該方法使用了ACC_PUBLIC,即pubilc方法。然後沒有了修飾符。
name_index爲0x000d,即指向常量池第13個 常量:
可以看到該方法名字叫 < init >
descriptor_ index爲0x000e,即指向常量池第14個常量:
可以看到該方法描述符號爲 ()V
attributes_count 值爲0x0001,表示具有一個額外屬性,那麼可以繼續向下找。
向後六個字節,查看attribute_info
attribute_name_index爲0x000f,即指向常量池第15個常量:
說明該方法具有名爲code的屬性!
attribute_length爲0x0000002f,即長度爲47,這就很長了,並且不同的屬性具有自己的格式,因此具體的分析,我們在屬性表集合中介紹!
2.10 屬性表集合
u2 attributes_count; //此類的屬性表中的屬性數量
attribute_info attributes[attributes_count]; //屬性表集合
屬性表用於class文件格式中的ClassFile,field_info,method_info和Code_attribute結構,以用於描述某些場景專有的信息。與 Class 文件中其它的數據項目要求的順序、長度和內容不同,屬性表集合的限制稍微寬鬆一些,不再要求各個屬性表具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以向屬性表中寫 入自己定義的屬性信息,Java 虛擬機運行時會忽略掉它不認識的屬性。
對於每個屬性,它的名稱需要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4的長度去說明屬性值所佔用的位數即可。
屬性表通用結構
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
對於任意屬性, attribute_name_index必須是對當前class文件的常量池的有效16位無符號索引。 常量池在該索引處的成員必須是CONSTANT_Utf8_info 結構, 用以表示當前屬性的名字。 attribute_length項的值給出了跟隨其後的信息字節的 長度, 這個長度不包括attribute_name_index 和 attribute_length項的6個字節。
2.10.1 預定義屬性
《java虛擬機規範 JavaSE8》中預定義23項虛擬機實現應當能識別的屬性:
屬性 | 可用位置 | 含義 |
SourceFile | ClassFile | 記錄源文件名稱 |
InnerClasses | ClassFile | 內部類列表 |
EnclosingMethod | ClassFile | 僅當一個類爲局部類或者匿名類時,才能擁有這個屬性,這個屬性用於表示這個類所在的外圍方法 |
SourceDebugExtension | ClassFile | JDK1.6中新增的屬性,SourceDebugExtension用於存儲額外的調試信息。如在進行JSP文件調試時,無法通過Java堆棧來定位到JSP文件的行號,JSR-45規範爲這些非Java語言編寫,卻需要編譯成字節碼運行在Java虛擬機匯中的程序提供了一個進行調試的標準機制,使用SourceDebugExtension就可以存儲這些調試信息。 |
BootstrapMethods | ClassFile | JDK1.7新增的屬性,用於保存invokedynamic指令引用的引導方法限定符 |
ConstantValue | field_info | final關鍵字定義的常量值 |
Code | method_info | Java代碼編譯成的字節碼指令(即:具體的方法邏輯字節碼指令) |
Exceptions | method_info | 方法聲明的異常 |
RuntimeVisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的屬性,爲動態註解提供支持。RuntimeVisibleAnnotations屬性,用於指明哪些註解是運行時(實際上運行時就是進行反射調用)可見的。 |
RuntimeInvisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的屬性,作用與RuntimeVisibleAnnotations相反用於指明哪些註解是運行時不可見的。 |
RuntimeVisibleParameterAnnotations | method_info | JDK1.5中新增的屬性,作用與RuntimeVisibleAnnotations類似,只不過作用對象爲方法的參數。 |
RuntimeInvisibleParameterAnnotations | method_info | JDK1.5中新增的屬性,作用與RuntimeInvisibleAnnotations類似,只不過作用對象爲方法的參數。 |
AnnotationDefault | method_info | JDK1.5中新增的屬性,用於記錄註解類元素的默認值 |
MethodParameters | method_info | 52.0 |
Synthetic | ClassFile, field_info, method_info | 標識方法或字段爲編譯器自動產生的 |
Deprecated | ClassFile, field_info, method_info | 被聲明爲deprecated的方法和字段 |
Signature | ClassFile, field_info, method_info | JDK1.5新增的屬性,這個屬性用於支持泛型情況下的方法簽名,在Java語言中,任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量(Type Variables)或參數類型(Parameterized Types),則Signature屬性會爲它記錄泛型簽名信息。由於Java的泛型採用擦除法實現,在爲了避免類型信息被擦除後導致簽名混亂,需要這個屬性記錄泛型中的相關信息 |
RuntimeVisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的屬性,爲動態註解提供支持。RuntimeVisibleAnnotations屬性,用於指明哪些註解是運行時(實際上運行時就是進行反射調用)可見的。 |
RuntimeInvisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的屬性,作用與RuntimeVisibleAnnotations相反用於指明哪些註解是運行時不可見的。 |
LineNumberTable | Code | Java源碼的行號與字節碼指令的對應關係 |
LocalVariableTable | Code | 方法的局部變量描述 |
LocalVariableTypeTable | Code | JDK1.5中新增的屬性,它使用特徵簽名代替描述符,是爲了引入泛型語法之後能描述泛型參數化類型而添加 |
StackMapTable | Code | JDK1.6中新增的屬性,供新的類型檢查驗證器(Type Checker)檢查和處理目標方法的局部變量和操作數棧所需要的類型是否匹配 |
MethodParameters | method_info | JDK1.8中新加的屬性,用於標識方法參數的名稱和訪問標誌。 |
RuntimeVisibleTypeAnnotations | ClassFile, field_info, method_info, Code | JDK1.8中新加的屬性,在運行時可見的註釋,用於泛型類型,指令等。 |
RuntimeInvisibleTypeAnnotations | ClassFile, field_info, method_info, Code | JDK1.8中新加的屬性,在編譯時可見的註釋,用於泛型類型,指令等。 |
這裏主講一些常見的屬性。
2.10.2 Code屬性——方法體
Java方法體裏面的代碼經過Javac編譯之後,最終變爲字節碼指令存儲在Code屬性內,Code屬性出現在在method_info結構的attributes表中,但在接口或抽象類中就不存在Code屬性(JDK1.8可以出現了)。一個方法中的Code屬性值有一個。
在整個Class文件中,Code屬性用於描述代碼,所有的其他數據項目都用於描述元數據。
在字節碼指令之後的是這個方法的顯式異常處理表(下文簡稱異常表)集合,異常表對於Code屬性來說並不是必須存在的。
Code屬性的結構如下:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Code屬性的第一個字段attribute_name_index指定了該屬性的名稱,它是一個指向常量池的索引, 指向的類型爲CONSTANT_Utf8_info, 對於Code 屬性來說,該值恆爲“Code"。
attribute_length指定了Code屬性的長度,該長度不包括前6個字節,即表示剩餘長度。
在方法執行過程中,操作數棧可能不停地變化,在整個執行過程中,操作數棧存在一個最大深度,該深度由max_stack表示。
在方法執行過程中,局部變量表也可能會不斷變化。在整個執行過程中局部變量表的最值由max_locals表示, 它們都是2字節的無符號整數。也包括調用此方法時用於傳遞參數的局部變量。
在max_locals 之後,就是作爲方法的最重要部分—字節碼。它由code_length和code[code_length]兩部分組成, code_length 表示字節碼的字節數,爲4字節無符號整數,必須大於0;code[code length]爲byte數組,即存放的代碼的實際字節內容本身。
在字節碼之後,存放該方法的異常處理表。異常處理表告訴一個方法該如何處理字節碼中可能拋出的異常。異常處理表亦由兩部分組成:表項數量和內容。在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的,而是採用異常表來完成的。
exception_table_length表示異常表的表項數量,可以爲0;
exception_table[exception_table_length]結構爲異常表的數據。
異常表中每一個數據由4部分組成,分別是start_pc、end_pc、handler_pc和catch_type。這4項表示從方法字節碼的start_pc偏移量開始(包括)到end_pc 偏移量爲止(不包括)的這段代碼中,如果遇到了catch_type所指定的異常, 那麼代碼就跳轉到handler_pc的位置執行,handler_pc即一個異常處理器的起點。
在這4項中, start_pc、end_pc和handlerpc 都是字節碼的編譯量, 也就是在code[code_length]中的位置, 而catch_type爲指向常量池的索引,它指向一個CONSTANT_Class_info 類,表示需要處理的異常類型。如果catch_type值爲0,那麼將會在所有異常拋出時都調用這個異常處理器,這被用於實現finally語句。
至此, Code屬性的主體部分已經介紹完畢, 但是Code屬性中還可能包含更多信息, 比如行號表、局部變量表等。這些信息都以attribute屬性的形式內嵌在Code屬性中, 即除了字段、方法和類文件可以內嵌屬性外,屬性本身也可以內嵌其他屬性。attributes_count表示Code屬性的屬性數量,attributes表示Code屬性包含的屬性。
2.10.3 LineNumberTable屬性——記錄行號
位於Code屬性中,描述Java源碼行號與字節碼行號(字節碼的偏移量)之間的對應關係。主要是如果拋出異常時,編譯器會顯示行號,比如調試程序時展示的行號,就是這個屬性的作用。
Code屬性表中,LineNumberTable可以屬性可以按照任意順序出現。
在Code屬性 attributes表中,可以有不止一個LineNumberTable屬性對應於源文件中的同一行。也就是說,多個LineNumberTable屬性可以合起來表示源文件中的某行代碼,屬性與源文件的代碼行之間不必有一一對應的關係。
LineNumberTable屬性的格式如下:
LineNumberTable_ attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
}line_number_table[line_number_table_length];
}
其中, attribute_name_index爲指向常量池的索引, 在LineNumberTable 屬性中, 該值爲"LineNumberTable", attribute_length爲4 字節無符號整數, 表示屬性的長度(不含前6個字節),line_number_table_length 表明了表項有多少條記錄。
line_number_table爲表的實際內容,它包含line_number_table_length 個<start_pc, line_number>元組,其中,start_pc爲字節碼偏移量,必須是code數組的一個索引, line_number 爲對應的源碼的行號。
2.10.4 LocalVariableTable屬性——保存局部變量和參數
描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的關係。用處在於當別人使用這個方法是能夠顯示出方法定義的參數名。
它也不是運行時必需的屬性,但默認會生成到Class文件之中。如果沒有生成這項屬性,最大的影響就是當其他人引用這個方法時,所有的參數名稱都將會丟失。
LocalVariableTable屬性結構如下:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
其中, attribute_name_index爲當前屬性的名字, 它是指向常量池的索引。對局部變量表而言, 該值爲“ LocalVariableTable", attribute_length 爲屬性的長度,local_variable_table_length爲局部變量表表項條目。
局部變量表的每一條記錄由以下幾個部分組成:
- start_pc、length:start_pc表示當前局部變量的開始位置,從0開始,length表示範圍覆蓋長度,兩者結合就是這個局部變量在字節碼中的作用域範圍。
- name_ index: 局部變量的名稱, 這是一個指向常量池的索引,爲CONSTANT_Utf8_info類型。
- descriptor_index: 局部變量的類型描述, 指向常量池的索引。使用和字段描述符一樣的方式描述局部變量,爲CONSTANT_Utf8_info類型。
- index,局部變量在當前幀棧的局部變量表中的槽位(solt)。當變量數據類型是64位時(long 和double), 它們會佔據局部變量表中的兩個槽位,位置爲index和index+1。
在JDK 1.5引入泛型後,LocalVariableTable屬性增加了一個“姐妹屬性”:LocalVariableTypeTable,這個新增的屬性結構與LocalVariableTable非常相似,僅僅是把記錄的字段描述符的descriptor_index替換成了字段的特徵簽名(Signature),對於非泛型類型來說,描述符和特徵簽名能描述的信息是基本一致的,但是泛型引入之後,由於描述符中泛型的參數化類型被擦出掉,描述符就不能準確地描述泛型類型了,因此出現了LocalVariableTypeTable。
2.10.5 StackMapTable屬性——加快字節碼校驗
JDK 1.6 以後的類文件, 每個方法的Code 屬性還可能含有一個StackMapTable 的屬性結構。這是一個複雜的變長屬性,位於Code屬性的屬性表中。這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用,目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器,加快字節碼校驗。
StackMapTable屬性中包含零至多個棧映射幀(Stack Map Frames),每個棧映射幀都顯式或隱式地代表了一個字節碼偏移量,用於表示該執行到該字節碼時局部變量表和操作數棧的驗證類型。
該屬性不包含運行時所需的信息,僅僅作爲Class文件的類型檢驗。
StackMapTable屬性結構如下:
StackMapTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_entries;
stack_map_frame entries[number_of_entries];
}
其中,attribute_name——index爲常量池索引, 恆爲“ Stack.MapTable", attribute_length爲該屬性的長度, number_of_entries爲棧映射幀的數量, 最後的stack_map_frame entries則爲具體的內容,每一項爲一個stack_map_ frame結構。
2.10.6 Exceptions屬性——列舉異常
列舉出方法中可能拋出的受查異常,也就是方法描述時在throws關鍵字後面列舉的異常。Exceptions與Code 屬性中的異常表不同。Exceptions 屬性表示一個方法可能拋出的異常,通常是由方法的throws 關鍵字指定的。而Code 屬性中的異常表,則是異常處理機制,由try-catch語句生成。
Exceptions屬性結構如下:
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
Exceptions 屬性表中, attribute_name_index 指定了屬性的名稱, 它爲指向常量池的索引,恆爲“Exceptions",attribute_lengt表示屬性長度, number_of_exceptions表示表項數量即可能拋出的異常個數,最後exception_index_table項羅列了所有的異常,每一項爲指向常量池的索引,對應的常量爲CONSTANT_Class_info,表示爲一個異常類型。
2.10.7 SourseFile屬性——記錄來源
SourseFile屬性ClassFile,記錄生成這個Class文件的源碼文件名稱,拋出異常時能夠顯示錯誤代碼所屬的文件名。
SourseFile屬性結構如下:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
SourseFile屬性表中, attribute_name_index 指定了屬性的名稱, 它爲指向常量池的索引,恆爲“SourseFile", attribute_length表示屬性長度,對SourseFile屬性來說恆爲2,sourcefile_index表示源代碼文件名, 它是爲指向常量池的索引,對應的常量爲CONSTANT_Uft8_info類型。
2.10.8 InnerClass屬性——記錄內部類
InnerClass屬性屬性ClassFile,用於記錄內部類與宿主類之間的關聯。
InnerClass屬性結構如下:
InnerClasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
{ u2 inner_class_info_index;
u2 outer_class_info_index;
u2 inner_name_index;
u2 inner_class_access_flags;
} classes[number_of_classes];
}
其中, attribute_name_index表示屬性名稱,爲指向常量池的索引,這裏恆爲“TnnerClasses ” 。attribute_ length 爲屬性長度,number_of_classes 表示內部類的個數。classes[number_ of_ classes]爲描述內部類的表格,每一條內部類記錄包含4個字段,其中,inner_class_ info_ index爲指向常量池的指針, 它指向一個CONSTANT_ Class_info, 表示內部類的類型。outer_class_ info_index表示外部類類型,也是常量池的索引。inner_name_ index 表示內部類的名稱, 指向常量池中的CONSTANT_ Utf8_info項。最後的inner_class_access_flags爲內部類的訪問標識符, 用於指示static、public等屬性。
內部內標識符表如下:
標記名 | 值 | 含義 |
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | 私有類 |
ACC_PROTECTED | 0x0004 | 受保護的類 |
ACC_STATIC | 0x0008 | 靜態內部類 |
ACC_FINAL | 0x0010 | fmal類 |
ACC_INTERFACE | 0x0200 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象類 |
ACC_SYNTHETIC | 0x1000 | 編譯器產生的,而非代碼產生的類 |
ACC_ANNOTATION | 0x2000 | 註釋 |
ACC_ENUM | 0x4000 | 枚舉 |
2.10.9 ConstantValue屬性——static(常量)標識
ConstantValue屬性位於filed_info屬性中,通知虛擬機自動爲靜態變量賦值,只有被static字符修飾的變量(類變量)纔可以有這項屬性。如果非靜態字段擁有了這一屬性,也該屬性會被虛擬機所忽略。
對於非static類型的變量(也就是實例變量)的賦值是在實例構造器<init>方法中進行的;而對於類變量,則有兩種方式可以選擇:在類構造器<clinit>方法中或者使用ConstantValue屬性。但是不同虛擬機有不同的實現。
目前Sun Javac編譯器的選擇是:如果同時使用final和static來修飾一個變量(按照習慣,這裏稱“常量”更貼切),並且這個變量的數據類型是基本類型或者java.lang.String的話,就生成ConstantValue屬性來進行初始化,即編譯的時候;如果這個變量沒有被final修飾,或者並非基本類型及字符串,則將會選擇在<clinit>方法中進行初始化,即類加載的時候。
ConstantValue屬性結構如下:
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
其中, attribute_name_index表示屬性名稱,爲指向常量池的索引,這裏恆爲“ConstantValue_attribute ” 。attribute_length表示後面還有幾個字節,這裏固定爲2。constantvalue_index代表了常量池中一個字面常量的引用,根據字段類型不同,字面量可以是CONSTANT_Long_info、 CONSTANT_Float_info、 CONSTANT_Double_info、CONSTANT_lnteger_info、CONSTANT_String_info常量中的一種。
2.10.11 Signature屬性
一個可選的定長屬性,可以出現於ClassFile, field_info, method_info結構的屬性表中。
任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則Signature屬性會爲它記錄泛型簽名信息。
Signature屬性結構如下:
Signature_attribute {
u2 attribute_name_index; //指向常量池的名稱引用,固定爲“Signature”
u4 attribute_length; // 固定爲2
u2 signature_index; //指向常量池的類簽名、方法類型前面、字段類型簽名引用
}
2.10.12 Deprecated屬性——廢棄
標誌類型的布爾屬性,Deprecated表示類、方法、字段不再推薦使用,使用註解表示爲@deprecated
2.10.13 Synthetic屬性——編譯器添加
標誌類型的布爾屬性,Synthetic屬性用在ClassFile, field_info, method_info中,表示此字段或方法或類是由編譯器自行添加的。
對於其他屬性,可以查看官方文檔:The Java® Virtual Machine Specification——Java SE 8 Edition
2.10.14 測試
2.10.14.1 Code屬性
我們接着上面的Class文件向下看,我們知道< init >方法中有一個Code屬性,Code屬性結構爲:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
首先看前兩個屬性(其實在方法表處已經分析出來了):
attribute_name_index爲0x000f,即指向常量池第15個常量。
該屬性名爲Code!
attribute_length 表示Code屬性剩餘長度 ,0x0000002f,即47個字節。
接着看後兩個屬性:
max_stack爲1,max_locals也爲1,即方法內部沒調用其他方法。
接着看後兩個屬性,即字節碼部分:
code_length 表示字節碼的長度,這裏是5, code[code length]爲byte數組, 爲字節碼內容本身,長度爲5個字節:
繼續向下,該方法實際上是用於對象創建的方法,實際上是沒有異常處理表的,因此長度自然爲0,下面的數組部分分字節碼爲0,如下圖:
exception_table_length爲0x0000,即0,那麼就不會出現exception_table,繼續向下兩個字節,表示Code屬性的屬性數量,即attributes_count:
attributes_count值爲0x0002,即有Code屬性內部有兩個屬性,下面我們來看看是哪兩個屬性!
2.10.14.2 Code內部第一個屬性
老樣子,向後走六個字節:
Code屬性內部的第一個屬性的attribute_name_index爲0x0010,即第16個常量;
Code屬性內部的第一個屬性的attribute_length爲0x00000006,即後接6字節;
通過查找javap的常量池可知,第16個常量屬性就是LineNumberTable屬性。
我們知道LineNumberTable屬性結構爲:
LineNumberTable_ attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
}line_number_table[line_number_table_length];
}
繼續,向下走六個字節,查看屬性後面部分:
line_number_table_length爲0x0001,即1,即line_number_table表的實際內容爲1個數據。下面進入這個數據內部:
start_pc爲0x0000,即字節碼偏移量爲0。
line_number爲0x0004,即對應的行號爲4(反編譯Class就會出現無參構造器,對應行號就是4)。
**到此,Code屬性內部的第一個屬性LineNumberTable結束。
2.10.14.3 Code內部第二個屬性
老樣子,第二個屬性先向後取六位字節:
Code屬性內部的第二個屬性的attribute_name_index爲0x0011,即第17個常量;
Code屬性內部的第二個屬性的attribute_length爲0x0000000c,即後接12字節;
通過查找javap的常量池可知,第17個常量屬性就是LocalVariableTable屬性。
我們知道LocalVariableTable屬性結構爲:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
向後再取兩個字節:
local_variable_table_length爲0x0001,即1,即局部變量表表項條目爲1條,後面一條數據,剛好十個佔據字節,進入local_variable_table的數據:
start_pc爲0x0000,即0,局部變量開始位置爲0,即佔用0位solt,實際上這個局部變量就是this(在運行時棧結構部分會有講解)。
length爲0x0005,即範圍覆蓋長度爲5。
name_index爲0x0012,即第18個常量。
可以看到這個局部變量名叫this,這個變量是虛擬機爲我們加到實例方法中的,可以直接使用,但是對於靜態方法,卻沒有這個局部變量。
descriptor_index爲0x0013,即第19個常量。
表示局部變量的類型描述,可以看到,該this變量的類型就是該類的類型。
index爲0x0000,即0,即this變量,所佔用的solt槽位是0。
到此,Code的兩個額外屬性尋找完畢,剛好47個字節,該< init >方法的額外屬性Code尋找完畢,該方法尋找完畢!
2.10.14.4 驗證
下面來驗證我們上面的分析:
我們的javap -v ClassFile.class 指令實際上也編譯出了方法表,我們找到init方法:
可以看到,我們通過計算class文件得到的信息和javap指令反編譯得到的信息是一致的!
對於後面的三個方法,就是< cinit >、setK、getK,不過是依葫蘆畫瓢,找到之後,得出結果,然後和javap的結果進行對比,看是不是正確的,在此不再贅述。
在最後,實際上最後8個字節表示的是SourseFile屬性,你們可以自己看看到底是不是!
3 總結
到此,我已經帶領大家把Class文件從頭到尾的大概梳理了一遍,以後遇到更加複雜的類,實際上可以直接使用javap指令查看反解析出的數據,那樣更加方便。關於javap指令和字節碼指令的查看,這裏沒有講解,可以參考官網:Verification of class Files,後面我也會處相應的中文教程。我們知道了Class文件的結構組成,對於我們後續的JVM深入學習是很有幫助的,比如編譯器的學習(這篇文章只是講解編譯之後的數據結構),我們以後可以自己實現一個Java編譯器!
如果有什麼不懂或者需要交流,各位可以留言。另外,希望點贊、收藏、關注一下,我將不間斷更新Java各種教程!