1. 程式人生 > >如何學好JVM征服面試官?一篇Class 類檔案結構你還不會嗎?

如何學好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 的方法、例項初始化方法以及類和介面的類初