如何學好JVM征服面試官?一篇Class 類文件結構你還不會嗎?

 

 

 

 

Java 跨平臺的基礎

各類不一樣平臺的虛擬機與全部平臺都統一使用的程序存儲格式——字節碼(ByteCode)是java

構成平臺無關性的基石,也是語言無關性的基礎。Java 虛擬機不和包括 Java 在內的任何數組

語言綁定,它只與「Class 文件」這種特定的二進制文件格式所關聯,Class 文件中包含了安全

Java 虛擬機指令集和符號表以及若干其餘輔助信息。數據結構

Class 類的本質

任何一個 Class 文件都對應着惟一一個類或接口的定義信息,但反過來講,Class 文件實ide

際上它並不必定以磁盤文件的形式存在。編碼

Class 文件是一組以 8 位字節爲基礎單位的二進制流。spa

Class 文件格式

各個數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符,這code

使得整個 Class 文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。對象

Class 文件格式採用一種相似於 C 語言結構體的僞結構來存儲數據,這種僞結構中只有兩blog

種數據類型:無符號數和表。

無符號數屬於基本的數據類型,以 u一、u二、u四、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_m一、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、dup二、

dup_x一、dup2_x一、dup_x二、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 的方法、實例初始化方法以及類和接口的類初始化方法使用。

相關文章
相關標籤/搜索