1. 程式人生 > >讀書筆記 ---- 《深入理解Java虛擬機器》---- 第5篇:類檔案結構

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第5篇:類檔案結構

上一篇:虛擬機器效能監控與故障處理工具:https://blog.csdn.net/pcwl1206/article/details/84197113

第5篇:類檔案結構

開篇說明:本文的重點就是類檔案結構,只需要清楚Class檔案格式中的各名稱的實際意義就行,不用對具體名稱下的細節進行深究,否則需要花大量的時間【----面試導向】。

5.1  無關性的基石

1、統一的程式儲存格式:不同平臺的虛擬機器在所有的平臺都統一使用程式儲存格式----位元組碼(ByteCode);

2、Java編譯器可以把Java程式程式碼編譯成虛擬機器所需要的Class檔案;

3、Java虛擬機器不關心Class檔案的來源,而只和“Class檔案“這種二進位制檔案格式關聯,也就是說Java虛擬機器只認識”Class“檔案。因此只要能夠編譯成規範的Class檔案的語言都可以在運用Java虛擬機器。


5.2  Class檔案的結構

下面放一張網上找到的class檔案內容,先直觀感受下Class檔案:【可以使用Sublime  Text編輯器開啟Class檔案】

位元組碼Class類檔案是由一些列位元組碼命令組成,用於表示程式中各種常量、變數、關鍵字和運算子號的語義等等。

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

Java虛擬機器規定,Class檔案格式採用一種類似於C語言結構體系的偽結構來儲存資料,這種偽結構中只有兩種資料型別:無符號數

1、無符號數:屬於基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串值;

2、表:是由多個無符號數或者其他表作為資料項構成的複合資料型別,所有表都習慣地以”_info“結尾。表用於描述有層次關係的複合結構的資料,整個Class檔案本質上就是一張表。

Class檔案結構如下標所示:

型別 名稱 數量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

Class檔案沒有任何分隔符,嚴格按照上面結構表中的順序排列。無論是順序還是數量,甚至於資料儲存的位元組序這樣的細節,都是被嚴格限定的,哪個位元組代表什麼含義,長度是多少,先後順序如何,都不允許改變。下面將對上表中的各個資料項的實際含義進行講解。

1、魔數:magic

每個Class檔案的頭4個位元組稱為魔數(Magic  Number),它的唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案,即判斷這個檔案是否符合Class檔案規範。使用魔數而不是副檔名來進行識別主要是基於安全方面的考慮,因為副檔名可以隨意地改動,而魔數值可以唯一地確定檔案的型別。

2、檔案的版本:minor_version和major_version

緊接著魔數的4個位元組儲存的是Class檔案的版本號,第5和第6個位元組是次版本號(Minor  Version),第7和第8個位元組是主版本號(Major  Version)

Class檔案的主、次版本號是由JDK的版本決定的,JDK1.0 ~ JDK1.1使用了45.0 ~ 45.3的版本號(45是主版本號,”.“之和的3是次版本號),從JDK1.1開始,每個大版本的JDK主版本號加1。

高版本的JDK能向下相容以前版本的Class檔案,但不能執行以後版本的Class檔案,即使檔案格式並未發生任何變化,虛擬機器也必須拒絕執行超過其版本號的Class檔案。

3、常量池:constant_pool_count和constant_pool

緊接著主次版本號之後的是常量次入口,常量池可以理解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時它還是Class檔案中第一個出現的表型別資料專案。

constant_pool_count代表Class檔案常量池容量計數值。由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2型別的資料代表常量池容量計數值。

常量池的計數從1開始,因此常量池的容量是constant_pool_count - 1。第0項常量空出來是有特殊考慮的,這樣做的目的在於滿足後面某些指向常量池的索引值的資料在特定情況下需要表達”不引用任何一個常量池專案“的含義,這種情況就可以把索引值置為0來表示。

常量池中主要存放兩大類常量:字面量(Literal)符號引用(Symbolic  References)

1、字面量:比較接近Java語言中的常量的概念,如:文字字串、宣告為final的常量值等;

2、符號引用:屬於編譯原理方面的概念,包括三類常量:類和介面的全限定名、欄位的名稱和描述符、方法的名和描述符。

常量池中每一項常量都是一個表,JDK1.7中有14種結構各不相同的表結構資料。這14種表都有一個共同的特點,就是表開始的第一位是一個u1型別的標誌位(tag),代表這個常量屬於哪種常量型別。

需要注意一點:

  • length:

1、定義:UTF-8編碼的字串長度是多少;

2、65535限制:Class檔案中方法、欄位等都需要引用CONSTANT_Utf-8_info型常量來描述名稱。所以CONSTANT_Utf-8_info型常量的最大長度也就是Java中方法、欄位名的最大長度。而這裡的最大長度就是length的最大值,即u2型別能表達的最大值65535(2^32)。所以Java程式中如果定義了超過64KB英文字元的變數或方法名,將會無法編譯。

使用:javap - verbose Class檔名,輸出Class檔案的位元組碼內容。如下所示:

原始檔:TestClass.java

public class TestClass{
	private int m;
	
	public int inc(){
		return m + 1;
	}
}

輸出的位元組碼檔案:

C:\Users\pc941\Desktop>javap -verbose TestClass
Classfile /C:/Users/pc941/Desktop/TestClass.class
  Last modified 2018-11-18; size 275 bytes
  MD5 checksum f0aa61ed3167d2e74002b04941d5d7e2
  Compiled from "TestClass.java"
public class TestClass
  SourceFile: "TestClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         //  TestClass.m:I
   #3 = Class              #17            //  TestClass
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = NameAndType        #5:#6          //  m:I
  #17 = Utf8               TestClass
  #18 = Utf8               java/lang/Object
{
  public TestClass();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int inc();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 5: 0
}

4、訪問標誌:access_flags

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

5、類索引、父類索引與介面索引集合:this_class、super_class和interfaces

  • 類索引this_class:u2資料型別,用於確定類的全限定類名;
  • 父類索引super_class:u2資料型別,用於確定父類的全限定類名;
  • 介面索引集合interfaces:用於確定介面的全限定名,由於java中可以實現多個介面,因此使用interface_count來儲存介面數量。

java.lang.Object類的索引為0(沒有父類),其他所有的Java類的父索引都不為0,因為所有的Java類都繼承了Object類。實際上類的索引就是描述了Class的extends和implements的關係。

6、欄位表集合:field_info、fields_count

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

  • 描述的資訊包括:

  1. 作用域:public、private、protected
  2. 例項還是類變數:static
  3. 可變性:final
  4. 併發可見性:volatile
  5. 是否可序列化:transient
  6. 欄位資料型別:基本型別、物件、陣列等
  7. 欄位名稱

fields_count欄位數目:表示Class檔案的類和例項變數總數。

  • 欄位表集合原則
  1. 不會列出超類、父類或父類介面繼承而來的欄位;
  2. 有可能列出原本Java程式碼中不存在的欄位(內部類會自動新增外部類例項的欄位,才能引用到外部類);
  3. Java語言中的欄位是無法過載的。

7、方法表集合:methods、methods_count

和欄位表集合差不多,方法表集合用來描述Class檔案中的方法,但是訪問標誌和屬性表集合和欄位表集合有所區別。

  • 訪問標誌
  1. volatile、transient關鍵字不可以修飾方法,方法表中少了這兩種標誌;
  2. synchronized、native、strictfp和abstract可以修飾方法,故方法表增加了這些對應的標誌。
  • Code屬性
  1. 方法體中的程式碼都放在了”Code“屬性裡了。
  • 方法集合原則
  1. 如果父類方法沒有在子類中重寫(Override),則方法表集合中就不會出現來自父類的方法資訊;
  2. 編譯器有可能自動新增一些方法,如類構造器<clinit>方法和例項構造器<init>。
  3. 過載(Override)一個方法,需要與原方法具有相同的方法名稱但是不同的特徵簽名,特徵簽名就是一個方法中各引數在常量池中的欄位符號引用集合。

8、屬性表集合:attributes、attributes_count

attribute_info屬性表是Class檔案格式中最具有擴充套件性的一種資料專案,用於存放field_info欄位表、method_info方法表以及Class檔案的專有資訊,屬性表不要求各個屬性有嚴格的順序,只要求不與已有的屬性名字重複即可。屬性表中存放的常用資訊如下:

屬性名稱 使用位置 含義
Code 方法表 Java程式碼編譯後的位元組碼指令儲存在Code屬性裡
Exception 方法表 方法描述時在throws後丟擲的異常
LineNumberTable Code屬性 Java原始碼行號和位元組碼指令行號之間的對應關係
LocalVariableTable Code屬性 用於描述棧幀中區域性變量表中的變數與Java原始碼中定義的變數之間的關係
SourceFile 類檔案 記錄Class檔案的原始碼檔名稱
ConstantValue 欄位表 通知虛擬機器自動為靜態變數賦值,被static關鍵字修飾的類變數才可以使用
InnerClasses 類檔案 記錄內部類與宿主類之間的關係
Deprecated 類、方法、欄位表 被宣告為Deprecated的類、方法或者欄位表示被程式作者定為不再推薦使用
Synthetic 欄位、方法 代表此欄位或者方法不是由Java原始碼直接產生的,而是由編譯器自行新增的
StackMapTable Code屬性 在虛擬機器類載入位元組碼驗證被新型別檢查驗證器使用
Signature 類、介面、初始化方法或者成員的泛型簽名 Signature會為它們記錄泛型簽名信息
BootstrapMethods 類檔案 用於儲存invokedynamic指令引用的引導方法限定符

下面對上表中的Code屬性進行進一步講解:

Code(儲存Java編譯後的位元組碼檔案)

  • max_stack:運算元棧深度的最大值,JVM執行時根據這個值來分配棧幀中的操作棧深度;
  • max_locals:代表了局部變量表所需要的記憶體空間。

(1)Slot:虛擬機器為區域性變數分配記憶體的最小單位;

byte、char、float、int、short、boolean、returnAddress:長度少於32位,佔1個slot;

double、long:64位。佔2個slot。

(2)當代碼超出一個區域性變數的作用域時,這個區域性變數所佔用的slot可以被其他的區域性變數所使用。

  • code_length:位元組碼長度
  • code:儲存位元組碼指令
  • 65535限制:虛擬機器規定了一個方法不允許超過65535條位元組碼,否則編譯不通過
  • 執行:執行過程中的資料交換、方法呼叫等操作都基於棧的
  • this關鍵字:在例項方法中通常可以有this關鍵字來引用當前物件的變數,這是因為Java編譯時在區域性變量表中自動增加了這個(this)區域性變數。

5.3  位元組碼指令簡介

1、位元組碼的組成

  • 操作碼(Opcode):i (助記符),代表Int型別資料操作.......等等;
  • 運算元(Operands):永遠都是一個數組型別的物件。

Java虛擬機器採用面向運算元棧而不是暫存器的架構,位元組碼指令集是一種指令集架構。放棄了運算元對齊,省略了填充的符號和間隔。

2、載入和儲存指令

將資料在棧幀中的區域性變數和運算元棧之間來回傳輸。

  • 將一個區域性變數載入到操作棧;
  • 將一個數值從運算元棧儲存到區域性變量表;
  • 將一個常量載入到運算元棧;
  • 擴充區域性變量表的訪問索引的指令;

3、運算指令

  • 將兩個運算元棧上的值進行某種特定運算,並把結果重新存入運算元棧;
  • Java虛擬機器沒有直接支援byte、short、char、boolean型別,都轉為int型別進行運算,使用int的指令代替。

4、型別轉換指令

  • 寬化轉換    小範圍 --> 大範圍
  • int到long、float、double
  • long到float、double
  • float到double
  • 窄化轉換    大範圍 --> 小範圍
  • 必須顯示的宣告轉換;
  • 有益處或者精度丟失的情況,但是不會丟擲異常。

5、同步指令

  • Java虛擬機器支援方法級同步和方法內部一段指令序列的同步,這兩種同步結構都使用管程(Monitor)來支援;
  • 如果設定了synchronized同步方法,那麼執行執行緒就要求先成功持有“管程”,然後才能執行方法,最後方法執行完成後,再釋放“管程”;
  • Java虛擬機器通過monitorenter和monitorexit兩個指令配對使用,另外編譯器會自動增加一個異常處理器。當出現異常時,這個異常處理器能夠捕獲到所有的異常,並且釋放“管程”,mointorexit指令響應。因此,monitorenter和monitorexit這兩個指令總是成對出現。

5.4  總結

之所以說一些“非Java”語言也是可以在JVM上執行,這是因為JVM只認識Class檔案,所以如果某種語言最終編譯出來的檔案是Class檔案,那麼對於JVM來說就是沒有區別的,但前提是按照Class檔案的結構來,不然也無法正常執行。Class定義了許多特定的基本資料型別和表結構,通過魔數讓JVM認識該檔案,版本號保證可以在要求的JDK版本上執行,在常量池中定義好常量,訪問標誌位確定訪問許可權,索引集合方便與外界Class保持聯絡,欄位表儲存我們定義好的變數,方法表儲存方法的資訊,屬性表儲存了上訴各種表中的屬性。其中記住Slot是區域性變數分配記憶體的最小單位,當程式超過作用域的時候,Slot可以被其他替換使用。

到這裡,程式碼還是靜態的儲存格式,程式要執行起來,還需要操作指令,也是右位元組碼儲存,包括操作碼和運算元。操作指令有載入和儲存指令、運算指令、型別轉換指令已經同步指令等等。


上一篇:虛擬機器效能監控與故障處理工具:https://blog.csdn.net/pcwl1206/article/details/84197113

下一篇:虛擬機器類載入機制:https://blog.csdn.net/pcwl1206/article/details/84260914

參考及推薦:

1、深入理解Java虛擬機器04--類結構檔案https://www.cnblogs.com/ganchuanpu/p/9429248.html

2、深入理解JVM虛擬機器4:Java class介紹與解析實踐:https://blog.csdn.net/a724888/article/details/78415593