深入理解虛擬機器之類檔案結構

SnailClimb發表於2018-05-04

Java面試通關手冊(Java學習指南),會一直完善下去,歡迎大家star以及和我一起完善。 github.com/Snailclimb/…

《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第二版》讀書筆記與常見面試題總結

本節常見面試題(推薦帶著問題閱讀,問題答案在文中都有提到):

簡單介紹一下Class類檔案結構(常量池主要存放的是那兩大常量?Class檔案的繼承關係是如何確定的?欄位表、方法表、屬性表主要包含那些資訊?)

1 概述

  計算機雖然只能識別0和1,但是越來越多的程式語言選擇了與作業系統和機器指令集無關無關的、平臺中立的格式作為程式編譯後的儲存格式。Java虛擬機器不和包括Java在內的任何語言繫結,只與 "Class檔案" 這種特定的二進位制檔案所關聯,Class檔案中包含了Java虛擬機器指令集合符號表以及若干其它輔助資訊。Java虛擬機器作為一個通用的、機器無關的執行平臺,任何其他語言都可以將其作為語言的產品交付媒介。  

語言無關性
  

2 Class類檔案結構

Class檔案是一組以8位位元組為基礎的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全部是程式執行的必要資料,沒有空隙存在。當遇到需要佔用8位位元組以上空間的資料項時,則會按照高位在前的方式分割成若干個8位位元組進行儲存。

Class檔案格式採用一種類似於C語言結構體的偽結構來儲存數,這種偽結構有兩種資料型別:無符號數

這裡需要重複提一下,Class檔案結構不像XML等描述語言,由於它沒有任何分割符號,所以無論是數量甚至於資料儲存的位元組序這樣的細節都被嚴格限定,哪個位元組代表什麼含義,長度是多少,先後順序如何,都不允許改變。

2.1 魔數與Class檔案版本

每個Class檔案的頭四個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接收的Class檔案。緊接著魔數的四個位元組儲存的是Class檔案的版本號:第五和第六是次版本號,第七和第八是主版本號

2.2 常量池

緊接著主次版本號之後的是常量池入口,常量池可以理解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時它還是在Class檔案中第一個出現的表型別資料專案。 常量池主要存放兩大常量:字面量和符號引用字面量比較接近於java語言層面的的常量概念,如文字字串、宣告為final的常量值等。而符號引用則屬於編譯原理方面的概念。包括下面三類常量:

  • 類和介面的全限定名
  • 欄位的名稱和描述符
  • 方法的名稱和描述符

2.3訪問標誌

在常量池結束之後,緊接著的兩個位元組代表訪問標誌,這個標誌用於識別一些類或者介面層次的訪問資訊,包括:這個Class是類還是介面,是否為public或者abstract型別,如果是類的話是否宣告為final等等。具體標誌位及標誌的含義如下圖所示:

訪問標誌

2.4 類索引、父類索引與介面索引集合

類索引、父類索引與介面索引集合都按順序排列在訪問標誌之後,Class檔案由這三項資料來確定這個類的繼承關係類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名,由於java語言的單繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的java類都有父類,因此除了java.lang.Object外,所有java類的父類索引都不為0介面索引集合用來描述這個類實現了那些介面,這些被實現的介面將按implents(如果這個類本身是介面的話則是extends)後的介面順序從左到右排列在介面索引集合中。

2.5 欄位表集合

欄位表(field info)用於描述介面或類中宣告的變數。欄位包括類級變數以及例項變數,但不包括在方法內部宣告的區域性變數。

我們可以想一想在java中描述一個欄位可以包含什麼資訊呢?

欄位的作用域(public ,private,protected修飾符),是例項變數還是類變數(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主記憶體讀寫)、可否被序列化(transient修飾符)、欄位資料型別、欄位名稱。上述這些資訊中,各個修飾符都是布林值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而欄位叫什麼名字、欄位被定義為什麼資料型別這些都是無法固定的,只能引用常量池中常量來描述。

2.6方法表集合

Class檔案儲存格式中對方法的描述與對欄位的描述幾乎採用了完全一致的方式。方法標的結構如同欄位表一樣,依次包括了訪問標誌、名稱索引、描述符索引、屬性表集合幾項。 在這裡稍微提一下,因為volatile修飾符和transient修飾符不可以修飾方法,所以方法表的訪問標誌中沒有這兩個對應的標誌,但是增加了synchronized、native、abstract等關鍵字修飾方法,所以也就多了這些關鍵字對應的標誌

2.7 屬性表結合

在Class檔案,欄位表,方法表中都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。與Class檔案中其它的資料專案要求的順序、長 度和內容不同,屬性表集合的限制稍微寬鬆一些,不再要求各個屬性表具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以向屬性表中寫 入自己定義的屬性資訊,Java虛擬機器執行時會忽略掉它不認識的屬性。

3 位元組碼指令簡介

3.1位元組碼與資料型別

在java虛擬機器的指令集中,大多數的指令都包含了其操作所對應的資料型別資訊,例如iload指令用於從區域性變數表中載入int型別的資料到運算元棧中,而fload指令載入的則是float型別的資料。這兩條指令的操作在虛擬機器內部可能是同一段程式碼實現的,但在Class檔案中它們必須擁有各自獨立的操作碼。

大部分的指令都沒有支援整數型別byte、char、short甚至沒有任何指令支援boolean型別。大多數對於byte、char、short、boolean型別的操作,實際上都是使用相應的int型別作為運算子型別。

3.2 載入和儲存指令

載入和儲存指令用於將資料在棧幀中的區域性變數表和運算元棧之間來回傳輸。

3.3 運算指令

運算或算術指令用於對運算元棧上的值進行某種特定運算,並把結果重新存入操作棧頂。 大體上算術指令可以分為兩種:對整型資料和對浮點資料進行運算指令。(由於沒有byte、char、short、boolean型別,所以對這類資料的運算應使用int型別指令代替)

運算指令
運算指令

3.4 型別轉換指令

型別轉換指令可以將兩種不同的數值型別進行相互轉換。(比如int型別轉換為float型別) 小範圍到大範圍型別安全轉換,無需顯式的轉換指令,否則必須顯式的使用轉換指令來完成。

3.5 物件建立與訪問指令

雖然類例項和陣列都是物件,但java虛擬機器對類例項和陣列的建立和操作使用了不同的位元組碼指令。

物件建立與訪問指令

3.6 運算元棧管理指令

如同運算元據結構中的棧一樣,java虛擬機器也提供了一些用於直接操作運算元棧的指令,如:

運算元棧管理指令

3.7 控制轉移指令

可以認為控制轉移指令就是在有條件或無條件地修改PC暫存器的值。

3.8 方法呼叫和返回指令

  • invokevirtual 指令用於呼叫物件的例項方法
  • invokeinterface指令用於呼叫介面方法
  • invokespecial指令用於呼叫一些需要特殊處理的例項方法
  • invokestatic指令用於呼叫類方法(static方法)
  • invokedynamic指令用於在執行時動態解析出呼叫點限定符所使用的方法。

方法呼叫指令與資料型別無關,而方法返回指令是根據返回值的型別區分的。

3.9 異常處理指令

在java虛擬機器中,處理異常(catch語句)不是由位元組碼指令來實現的,而是採用異常表的方式。

3.10 同步指令

java虛擬機器可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構使用管程(Monitor)來支援的。

4 虛擬機器實現的兩種方式

  • 將輸入的java虛擬機器程式碼在載入或執行時翻譯成另外一種虛擬機器的指令集
  • 將輸入的java虛擬機器程式碼在載入或執行時翻譯成宿主主機CPU的本地指令集(即JIT程式碼生成技術)

5 class檔案結構的發展

Class檔案結構已經有十多年曆史了,這10多年間,java技術體系有了翻天覆地的變化,但是Class檔案結構一直處於比較穩定的狀態,Class檔案的主體結構、位元組碼指令的語義和數量幾乎沒有出現過變動,所有Class檔案格式的改進,都集中在向訪問標誌、屬性表這些在設計上就可擴充套件的資料結構中新增內容。

總結:

Class檔案是java虛擬機器執行引擎的資料入口,也是java技術體系的基礎構成之一。本節主要介紹了Class檔案結構中的各個部分,以及每個部分的定義、資料結構和使用方法。

歡迎關注我的微信公眾號:"Java面試通關手冊"(一個有溫度的微信公眾號,期待與你共同進步~~~堅持原創,分享美文,分享各種Java學習資源):

微信公眾號

相關文章