1. 程式人生 > >【搞定Jvm面試】 面試官:談談 JVM 類檔案結構的認識

【搞定Jvm面試】 面試官:談談 JVM 類檔案結構的認識

類檔案結構

一 概述

在 Java 中,JVM 可以理解的程式碼就叫做位元組碼(即副檔名為 .class 的檔案),它不面向任何特定的處理器,只面向虛擬機器。Java 語言通過位元組碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 Java 程式執行時比較高效,而且,由於位元組碼並不針對一種特定的機器,因此,Java 程式無須重新編譯便可在多種不同作業系統的計算機上執行。

Clojure(Lisp 語言的一種方言)、Groovy、Scala 等語言都是執行在 Java 虛擬機器之上。下圖展示了不同的語言被不同的編譯器編譯成.class檔案最終執行在 Java 虛擬機器之上。.class

檔案的二進位制格式可以使用 WinHex 檢視。

可以說.class檔案是不同的語言在 Java 虛擬機器之間的重要橋樑,同時也是支援 Java 跨平臺很重要的一個原因。

二 Class 檔案結構總結

根據 Java 虛擬機器規範,類檔案由單個 ClassFile 結構組成:

ClassFile {
    u4             magic; //Class 檔案的標誌
    u2             minor_version;//Class 的小版本號
    u2             major_version;//Class 的大版本號
    u2             constant_pool_count;//常量池的數量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的訪問標記
    u2             this_class;//當前類
    u2             super_class;//父類
    u2             interfaces_count;//介面
    u2             interfaces[interfaces_count];//一個類可以實現多個介面
    u2             fields_count;//Class 檔案的欄位屬性
    field_info     fields[fields_count];//一個類會可以有個欄位
    u2             methods_count;//Class 檔案的方法數量
    method_info    methods[methods_count];//一個類可以有個多個方法
    u2             attributes_count;//此類的屬性表中的屬性數
    attribute_info attributes[attributes_count];//屬性表集合
}

下面詳細介紹一下 Class 檔案結構涉及到的一些元件。

Class檔案位元組碼結構組織示意圖 (之前在網上儲存的,非常不錯,原出處不明):

2.1 魔數

    u4             magic; //Class 檔案的標誌

每個 Class 檔案的頭四個位元組稱為魔數(Magic Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接收的 Class 檔案。

程式設計者很多時候都喜歡用一些特殊的數字表示固定的檔案型別或者其它特殊的含義。

2.2 Class 檔案版本

    u2             minor_version;//Class 的小版本號
    u2             major_version;//Class 的大版本號

緊接著魔數的四個位元組儲存的是 Class 檔案的版本號:第五和第六是次版本號,第七和第八是主版本號。

高版本的 Java 虛擬機器可以執行低版本編譯器生成的 Class 檔案,但是低版本的 Java 虛擬機器不能執行高版本編譯器生成的 Class 檔案。所以,我們在實際開發的時候要確保開發的的 JDK 版本和生產環境的 JDK 版本保持一致。

2.3 常量池

    u2             constant_pool_count;//常量池的數量
    cp_info        constant_pool[constant_pool_count-1];//常量池

緊接著主次版本號之後的是常量池,常量池的數量是 constant_pool_count-1(常量池計數器是從1開始計數的,將第0項常量空出來是有特殊考慮的,索引值為0代表“不引用任何一個常量池項”)。

常量池主要存放兩大常量:字面量和符號引用。字面量比較接近於 Java 語言層面的的常量概念,如文字字串、宣告為 final 的常量值等。而符號引用則屬於編譯原理方面的概念。包括下面三類常量:

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

常量池中每一項常量都是一個表,這14種表有一個共同的特點:開始的第一位是一個 u1 型別的標誌位 -tag 來標識常量的型別,代表當前這個常量屬於哪種常量型別.

型別 標誌(tag) 描述
CONSTANT_utf8_info 1 UTF-8編碼的字串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 長整型字面量
CONSTANT_Double_info 雙精度浮點型字面量
CONSTANT_Class_info 類或介面的符號引用
CONSTANT_String_info 字串型別字面量
CONSTANT_Fieldref_info 欄位的符號引用
CONSTANT_Methodref_info 10 類中方法的符號引用
CONSTANT_InterfaceMethodref_info 11 介面中方法的符號引用
CONSTANT_NameAndType_info 12 欄位或方法的符號引用
CONSTANT_MothodType_info 16 標誌方法型別
CONSTANT_MethodHandle_info 15 表示方法控制代碼
CONSTANT_InvokeDynamic_info 18 表示一個動態方法呼叫點

.class 檔案可以通過javap -v class類名 指令來看一下其常量池中的資訊(javap -v class類名-> temp.txt :將結果輸出到 temp.txt 檔案)。

2.4 訪問標誌

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

類訪問和屬性修飾符:

我們定義了一個 Employee 類

package top.snailclimb.bean;
public class Employee {
   ...
}

通過javap -v class類名 指令來看一下類的訪問標誌。

2.5 當前類索引,父類索引與介面索引集合

    u2             this_class;//當前類
    u2             super_class;//父類
    u2             interfaces_count;//介面
    u2             interfaces[interfaces_count];//一個雷可以實現多個介面

類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名,由於 Java 語言的單繼承,所以父類索引只有一個,除了 java.lang.Object 之外,所有的 java 類都有父類,因此除了 java.lang.Object 外,所有 Java 類的父類索引都不為 0。

介面索引集合用來描述這個類實現了那些介面,這些被實現的介面將按implents(如果這個類本身是介面的話則是extends) 後的介面順序從左到右排列在介面索引集合中。

2.6 欄位表集合

    u2             fields_count;//Class 檔案的欄位的個數
    field_info     fields[fields_count];//一個類會可以有個欄位

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

field info(欄位表) 的結構:

  • access_flags: 欄位的作用域(public ,private,protected修飾符),是例項變數還是類變數(static修飾符),可否被序列化(transient 修飾符),可變性(final),可見性(volatile 修飾符,是否強制從主記憶體讀寫)。
  • name_index: 對常量池的引用,表示的欄位的名稱;
  • descriptor_index: 對常量池的引用,表示欄位和方法的描述符;
  • attributes_count: 一個欄位還會擁有一些額外的屬性,attributes_count 存放屬性的個數;
  • attributes[attributes_count]: 存放具體屬性具體內容。

上述這些資訊中,各個修飾符都是布林值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而欄位叫什麼名字、欄位被定義為什麼資料型別這些都是無法固定的,只能引用常量池中常量來描述。

欄位的 access_flags 的取值:

2.7 方法表集合

    u2             methods_count;//Class 檔案的方法的數量
    method_info    methods[methods_count];//一個類可以有個多個方法

methods_count 表示方法的數量,而 method_info 表示的方法表。

Class 檔案儲存格式中對方法的描述與對欄位的描述幾乎採用了完全一致的方式。方法表的結構如同欄位表一樣,依次包括了訪問標誌、名稱索引、描述符索引、屬性表集合幾項。

method_info(方法表的) 結構:

方法表的 access_flag 取值:

注意:因為volatile修飾符和transient修飾符不可以修飾方法,所以方法表的訪問標誌中沒有這兩個對應的標誌,但是增加了synchronizednativeabstract等關鍵字修飾方法,所以也就多了這些關鍵字對應的標誌。

2.8 屬性表集合

   u2             attributes_count;//此類的屬性表中的屬性數
   attribute_info attributes[attributes_count];//屬性表集合

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

參考

  • https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
  • https://coolshell.cn/articles/9229.html
  • https://blog.csdn.net/luanlouis/article/details/39960815
  • 《實戰 Java 虛擬機器》