來源:拿筆小星_ ,java
blog.csdn.net/u013096088/article/details/83047282數組
這篇博客開始,我打算帶你們去解讀一下JVM平臺下的字節碼文件(熟悉而又陌生的感受)。衆所周知,Class文件包含了咱們定義的類或接口的信息。而後字節碼又會被JVM加載到內存中,供JVM使用。那麼,類信息到了字節碼文件裏,它們如何表示的,以及在字節碼裏是怎麼分佈的呢?帶着這些問題,讓咱們去深刻了解字節碼文件吧。數據結構
Class文件的結構jvm
Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。當遇到須要佔用8位字節以上空間地數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。工具
每個 Class 文件對應於一個以下所示的 ClassFile 結構體。this
ClassFile {編碼
u4 magic;spa
u2 minor_version;.net
u2 major_version;設計
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
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];
}
這種數據結構,相似C語言結構體。這個結構體中只有兩種數據類型:無符號數和表,後面的解析都要以這兩種數據類型爲基礎,因此這裏要先介紹這兩個概念。
無符號數屬於基本的數據類型,以u1,u2,u4,u8來分別表明1個字節,2個字節,4個字節和8個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
表是由多個無符號數或者其餘表做爲數據項構成的複合數據類型,全部表都習慣性地以「_info」結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質就是一張表。
下面是個人案例代碼,本章將以此代碼生成的字節碼文件做爲例子來分析。
public class MyTest2 {
String str = "Welcome";
private int x = 5;
public static Integer in = 10;
public static void main(String[] args) {
MyTest2 myTest2 = new MyTest2();
myTest2.setX(8);
in = 20;
}
public void setX(int x) {
this.x = x;
}
}
對應生成的字節碼文件格式以下:(數據內容較多,只是截了部分)
上面的數字是以16進製表示的。咱們能夠按照以前的結構一項項去解讀它。
Class文件解析
magic
魔數,u4類型的數據,佔4個字節。魔數的惟一做用是肯定這個文件是否爲一個能被虛擬機所接受的 Class 文件。魔數值固定爲 0xCAFEBABE(咖啡寶貝),不會改變。
minor_version、major_version
緊接着魔數以後的4個字節爲Java版本信息:第5和第6個字節是次版本號(minor_version),第7和第8個字節是主版本號(major_version)。
就看當前這個字節碼,次版本號是0×0000=0,主版本號是0×0034=52。我本地機器用的是JDK1.8,因此可生成的Class文件主版本號最大值爲52.0。
下面給出了Java各個主版本號,以供參考。
constant_pool_count
常量池計數器,u2類型的數據。它是常量池的入口,表示緊跟着它後面的常量池的元素個數。算一下,0x002F=47,即常量池裏的元素有47個。這裏我用jdk的內置工具javap,反編譯一下,能夠輸出常量池的信息以及元素個數。執行命令:javap -verbose。輸出結果以下:
Constant pool:
#1 = Methodref #10.#34 // java/lang/Object."<init>":()V
#2 = String #35 // Welcome
#3 = Fieldref #5.#36 // com/shengsiyuan/jvm/bytecode/MyTest2.str:Ljava/lang/String;
#4 = Fieldref #5.#37 // com/shengsiyuan/jvm/bytecode/MyTest2.x:I
#5 = Class #38 // com/shengsiyuan/jvm/bytecode/MyTest2
#6 = Methodref #5.#34 // com/shengsiyuan/jvm/bytecode/MyTest2."<init>":()V
#7 = Methodref #5.#39 // com/shengsiyuan/jvm/bytecode/MyTest2.setX:(I)V
#8 = Methodref #40.#41 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#9 = Fieldref #5.#42 // com/shengsiyuan/jvm/bytecode/MyTest2.in:Ljava/lang/Integer;
#10 = Class #43 // java/lang/Object
#11 = Utf8 str
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 x
#14 = Utf8 I
#15 = Utf8 in
#16 = Utf8 Ljava/lang/Integer;
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lcom/shengsiyuan/jvm/bytecode/MyTest2;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 myTest2
#29 = Utf8 setX
#30 = Utf8 (I)V
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 MyTest2.java
#34 = NameAndType #17:#18 // "<init>":()V
#35 = Utf8 Welcome
#36 = NameAndType #11:#12 // str:Ljava/lang/String;
#37 = NameAndType #13:#14 // x:I
#38 = Utf8 com/shengsiyuan/jvm/bytecode/MyTest2
#39 = NameAndType #29:#30 // setX:(I)V
#40 = Class #44 // java/lang/Integer
#41 = NameAndType #45:#46 // valueOf:(I)Ljava/lang/Integer;
#42 = NameAndType #15:#16 // in:Ljava/lang/Integer;
#43 = Utf8 java/lang/Object
#44 = Utf8 java/lang/Integer
#45 = Utf8 valueOf
#46 = Utf8 (I)Ljava/lang/Integer;
但是,咱們獲得的常量池裏的元素個數是46。咱們看常量池第一個元素,它的索引是從1開始的。因此索引值範圍是1~46。設計者將第0項常量空出來是有特殊考慮的,這樣作的目的在於知足後面某些指向常量池的索引值的數據在特定狀況下須要表達「不引用任何一個常量池項目」的含義,這種狀況就能夠把索引值置爲0來表示。根本緣由在於,索引爲0也是一個常量(保留常量),只不過它不位於常量表中。這個常量就對應Null值,因此常量池的索引從1而非0開始。
常量池結構剖析
緊接其後的就是常量池了。一個Java類中定義的不少信息都是由常量池維護和描述的。能夠將常量池看做是Class文件的資源庫。好比:Java類中定義的方法與變量信息,都是存儲在常量池中。常量池中主要存儲兩類常量:字面常量和符號引用。字面量,如文本字符串,Java中聲明爲常量值,而符號引用如類和接口的全侷限定名,字段的名稱和描述符,方法的名稱和描述符等。
注:常量池中存儲的不必定是不變的量!如,private int x = 5,x是變量,但「x」這個變量名字依然存在常量池中。
咱們也能夠把常量池當作一個數組(常量池中的每一項常量都是一個表),與通常數組不一樣的是,常量池數組中不一樣的元素類型,結構都是不一樣的,長度固然也不相同;可是每個元素的第一個數據都是u1類型,該字節是個標誌位,佔一個字節。JVM在解析長量池時,會根據這個u1類型來獲取元素的具體類型。目前,常量池中出現的常量類型有14種,以下表:
有了這張表就能夠繼續剖析常量池的內容了,常量池第一個字節就是一個標誌位,0x000A=10,說明第一個常量類型是CONSTANT_Methodref_info。這是一個表類型,它對應的結構是:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
可知,該類型常量佔1+2+2=5個字節。因此咱們從常量池前5個字節就是第一個常量元素了。緊接後面就是第二個常量,一樣的,開始是一個標誌位,即0x008=8。可知,第二個常量是CONSTANT_String_info類型。CONSTANT_String_info 用於表示java.lang.String類型的常量對象,格式以下:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
因此常量池的第二個元素佔3個字節。按照這個套路,咱們就能夠找出每個常量了。一直數到第46個常量,常量池就結束了。此處是常量池中的14種常量項的結構總表。感興趣的能夠對照這個表,去把剩下的常量對照出來。
常量項分析
第一個常量是CONSTANT_Methodref_info類型的,它描述了類中方法的符號引用。class_index 項的值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Class_info結構,表示一個類或接口。
class_index表示的索引值是0x000A=10。根據以前 javap -verbose 輸出的常量池信息,咱們能夠知道常量池的#10項是CONSTANT_Class_info類型的常量。該類型常量用於表示類或接口,格式以下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
name_index 項的值,必須是對常量池的一個有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表明一個有效的類或接口二進制名稱的內部形式。
name_index 表示的索引值是43(這裏我直接從上面的量池信息讀出,若是從字節碼裏看,此處的值爲0x002B=43)。因此接着找常量池第43項的常量類型,是CONSTANT_utf8_info類型,用於表示字符串常量的值,結構以下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
其中,length 項的值指明瞭 bytes[]數組的長度,bytes[]是表示字符串值的byte數組。在這裏,我把字節碼常量池中#43處常量的16進制值單獨拿出來來看。下圖有背景色的部分就是完整的CONSTANT_Utf8_info類型常量表示。
第一個字節是標誌位,0×0001=1。說明此常量類型是CONSTANT_Utf8_info。後面2個字節是0×0010=16,表示後面bytes[]長度爲16。因此日後數16個字節就是整個它表示的字符串常量。
bytes[]第一個字節值,0x006A。根據 ASCII碼對照表,表明的字符串是」j」。依次的,第二個字節0×0061,表明「a」,等等。把16個字節看完你就獲得了字符串常量表示「java/lang/Object」。好了這表示一個類的全限定名。饒了一大圈,終於找到最終要表示的常量信息了。
到此,咱們把第一個常量的結構中的class_index就解析完了,還剩一個name_and_type_index。它表示了常量池在該索引處的項必須是 CONSTANT_NameAndType_info結構,它表示當前字段或方法的名字和描述符。後面你們能夠根據常量池中的14種常量項的結構總表,並結合javap獲得的常量池信息,本身去分析每一個常量在常量池裏是怎麼個回事。
總結
這篇文章介紹了,字節碼文件的結構組成,並分析了魔數、次主版本號和常量池。尤爲帶你們深刻分析了常量池的組成結構,並拿例子中的常量池第一個常量做爲案例,完整解析它在常量池中的各項引用。套路都是同樣的,常量池後面的常量,你們能夠本身去分析了。你會發現類中有用的信息都存在了咱們的常量池裏,而後以索引的形式,給代碼使用。這也就是常量池做爲class文件的資源倉庫的緣由了。