如何學好JVM征服面試官?一篇Class 類檔案結構你還不會嗎?
Java 跨平臺的基礎
各種不同平臺的虛擬機器與所有平臺都統一使用的程式儲存格式——位元組碼(ByteCode)是
構成平臺無關性的基石,也是語言無關性的基礎。Java 虛擬機器不和包括 Java 在內的任何
語言繫結,它只與“Class 檔案”這種特定的二進位制檔案格式所關聯,Class 檔案中包含了
Java 虛擬機器指令集和符號表以及若干其他輔助資訊。
Class 類的本質
任何一個 Class 檔案都對應著唯一一個類或介面的定義資訊,但反過來說,Class 檔案實
際上它並不一定以磁碟檔案的形式存在。
Class 檔案是一組以 8 位位元組為基礎單位的二進位制流。
Class 檔案格式
各個資料專案嚴格按照順序緊湊地排列在 Class 檔案之中,中間沒有新增任何分隔符,這
使得整個 Class 檔案中儲存的內容幾乎全部是程式執行的必要資料,沒有空隙存在。
Class 檔案格式採用一種類似於 C 語言結構體的偽結構來儲存資料,這種偽結構中只有兩
種資料型別:無符號數和表。
無符號數屬於基本的資料型別,以 u1、u2、u4、u8 來分別代表 1 個位元組、2 個位元組、4
個位元組和 8 個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照
UTF-8 編碼構成字串值。
表是由多個無符號數或者其他表作為資料項構成的複合資料型別,所有表都習慣性地以
“_info”結尾。表用於描述有層次關係的複合結構的資料,整個 Class 檔案本質上就是一
張表。
Class 檔案格式詳解
Class 的結構不像 XML 等描述語言,由於它沒有任何分隔符號,所以在其中的資料項,無
論是順序還是數量,都是被嚴格限定的,哪個位元組代表什麼含義,長度是多少,先後順序
如何,都不允許改變。
按順序包括:
魔數與 Class 檔案的版本
每個 Class 檔案的頭 4 個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案
是否為一個能被虛擬機器接受的 Class 檔案。使用魔數而不是副檔名來進行識別主要是基於
安全方面的考慮,因為副檔名可以隨意地改動。檔案格式的制定者可以自由地選擇魔
數值,只要這個魔數值還沒有被廣泛採用過同時又不會引起混淆即可。
緊接著魔數的 4 個位元組儲存的是 Class 檔案的版本號:第 5 和第 6 個位元組是次版本號
(MinorVersion),第 7 和第 8 個位元組是主版本號(Major Version)。Java 的版本號是從
45 開始的,JDK 1.1 之後的每個 JDK 大版本釋出主版本號向上加 1 高版本的 JDK 能向下
相容以前版本的 Class 檔案,但不能執行以後版本的 Class 檔案,即使檔案格式並未發生
任何變化,虛擬機器也必須拒絕執行超過其版本號的 Class 檔案。
常量池
常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項 u2 型別的資料,代
表常量池容量計數值(constant_pool_count)。與 Java 中語言習慣不一樣的是,這個容
量計數是從 1 而不是 0 開始的
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)。
字面量比較接近於 Java 語言層面的常量概念,如文字字串、宣告為 final 的常量值等。
而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:
類和介面的全限定名(Fully Qualified Name)、欄位的名稱和描述符(Descriptor)、方
法的名稱和描述符
訪問標誌
用於識別一些類或者介面層次的訪問資訊,包括:這個 Class 是類還是介面;是否定義為
public 型別;是否定義為 abstract 型別;如果是類的話,是否被宣告為 final 等
類索引、父類索引與介面索引集合
這三項資料來確定這個類的繼承關係。類索引用於確定這個類的全限定名,父類索引用於
確定這個類的父類的全限定名。由於 Java 語言不允許多重繼承,所以父類索引只有一
個,除了 java.lang.Object 之外,所有的 Java 類都有父類,因此除了 java.lang.Object
外,所有 Java 類的父類索引都不為 0。介面索引集合就用來描述這個類實現了哪些介面,
這些被實現的介面將按 implements 語句(如果這個類本身是一個介面,則應當是 extends
語句)後的介面順序從左到右排列在介面索引集合中
欄位表集合
描述介面或者類中宣告的變數。欄位(field)包括類級變數以及例項級變數。
而欄位叫什麼名字、欄位被定義為什麼資料型別,這些都是無法固定的,只能引用常量池
中的常量來描述。
欄位表集合中不會列出從超類或者父介面中繼承而來的欄位,但有可能列出原本 Java 代
碼之中不存在的欄位,譬如在內部類中為了保持對外部類的訪問性,會自動新增指向外部
類例項的欄位。
方法表集合
描述了方法的定義,但是方法裡的 Java 程式碼,經過編譯器編譯成位元組碼指令後,存放在
屬性表集合中的方法屬性表集合中一個名為“Code”的屬性裡面。
與欄位表集合相類似的,如果父類方法在子類中沒有被重寫(Override),方法表集合中
就不會出現來自父類的方法資訊。但同樣的,有可能會出現由編譯器自動新增的方法,最
典型的便是類構造器“<clinit>”方法和例項構造器“<init>”
屬性表集合
儲存 Class 檔案、欄位表、方法表都自己的屬性表集合,以用於描述某些場景
專有的資訊。如方法的程式碼就儲存在 Code 屬性表中。
位元組碼指令
Java 虛擬機器的指令由一個位元組長度的、代表著某種特定操作含義的數字(稱為操作碼,
Opcode)以及跟隨其後的零至多個代表此操作所需引數(稱為運算元,Operands)而構
成。由於限制了 Java 虛擬機器操作碼的長度為一個位元組(即 0~255),這意味著指令集的操作
碼總數不可能超過 256 條。
大多數的指令都包含了其操作所對應的資料型別資訊。例如:
iload 指令用於從區域性變量表中載入 int 型的資料到運算元棧中,而 fload 指令載入的則是
float 型別的資料。
大部分的指令都沒有支援整數型別 byte、char 和 short,甚至沒有任何指令支援 boolean
型別。大多數對於 boolean、byte、short 和 char 型別資料的操作,實際上都是使用相應
的 int 型別作為運算型別
閱讀位元組碼作為了解 Java 虛擬機器的基礎技能,請熟練掌握。請熟悉並掌握常見指令即
可。
載入和儲存指令
用於將資料在棧幀中的區域性變量表和運算元棧之間來回傳輸,這類指令包括如下內容。
將一個區域性變數載入到操作棧:iload、iload_<n>、lload、lload_<n>、fload、fload_
<n>、dload、dload_<n>、aload、aload_<n>。
將一個數值從運算元棧儲存到區域性變量表:istore、istore_<n>、lstore、lstore_<n>、
fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
將一個常量載入到運算元棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、
iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
擴充區域性變量表的訪問索引的指令:wide。
運算或算術指令
用於對兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。
加法指令:iadd、ladd、fadd、dadd。
減法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul 等等
型別轉換指令
可以將兩種不同的數值型別進行相互轉換,
Java 虛擬機器直接支援以下數值型別的寬化型別轉換(即小範圍型別向大範圍型別的安全轉
換):
int 型別到 long、float 或者 double 型別。
long 型別到 float、double 型別。
float 型別到 double 型別。
處理窄化型別轉換(Narrowing Numeric Conversions)時,必須顯式地使用轉換指令來完
成,這些轉換指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。
建立類例項的指令:
new。
建立陣列的指令:
newarray、anewarray、multianewarray。
訪問欄位指令:
getfield、putfield、getstatic、putstatic。
陣列存取相關指令
把一個數組元素載入到運算元棧的指令:baload、caload、saload、iaload、laload、
faload、daload、aaload。
將一個運算元棧的值儲存到陣列元素中的指令:bastore、castore、sastore、iastore、
fastore、dastore、aastore。
取陣列長度的指令:arraylength。
檢查類例項型別的指令:
instanceof、checkcast。
運算元棧管理指令
如同操作一個普通資料結構中的堆疊那樣,Java 虛擬機器提供了一些用於直接操作運算元棧
的指令,包括:將運算元棧的棧頂一個或兩個元素出棧:pop、pop2。
複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、
dup_x1、dup2_x1、dup_x2、dup2_x2。
將棧最頂端的兩個數值互換:swap。
控制轉移指令
控制轉移指令可以讓 Java 虛擬機器有條件或無條件地從指定的位置指令而不是控制轉移指
令的下一條指令繼續執行程式,從概念模型上理解,可以認為控制轉移指令就是在有條件
或無條件地修改 PC 暫存器的值。控制轉移指令如下。
條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、
if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
複合條件分支:tableswitch、lookupswitch。
無條件分支:goto、goto_w、jsr、jsr_w、ret。
方法呼叫指令
invokevirtual 指令用於呼叫物件的例項方法,根據物件的實際型別進行分派(虛方法分
派),這也是 Java 語言中最常見的方法分派方式。
invokeinterface 指令用於呼叫介面方法,它會在執行時搜尋一個實現了這個介面方法的對
象,找出適合的方法進行呼叫。
invokespecial 指令用於呼叫一些需要特殊處理的例項方法,包括例項初始化方法、私有方
法和父類方法。
invokestatic 指令用於呼叫類方法(static 方法)。
invokedynamic 指令用於在執行時動態解析出呼叫點限定符所引用的方法,並執行該方
法,前面 4 條呼叫指令的分派邏輯都固化在 Java 虛擬機器內部,而 invokedynamic 指令的
分派邏輯是由使用者所設定的引導方法決定的。
方法呼叫指令與資料型別無關。
方法返回指令
是根據返回值的型別區分的,包括 ireturn(當返回值是 boolean、byte、char、short 和 int
型別時使用)、lreturn、freturn、dreturn 和 areturn,另外還有一條 return 指令供宣告為
void 的方法、例項初始化方法以及類和介面的類初