1. 程式人生 > >讀薄《深入理解 Java 虛擬機器》 類檔案結構

讀薄《深入理解 Java 虛擬機器》 類檔案結構

Class 檔案是一組以 8 位元組為基礎單位的二進位制流,各項資料以嚴格的順序排列,中間沒有任何分隔符,使得整個 class 檔案中儲存的內容幾乎是程式執行的全部必要資料。

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

無符號數

基本的資料型別,以 u1,u2,u4,u8 來表示 1 個位元組,2 個位元組,4個位元組,8個位元組的無符號數。

由多個無符號數或者其他作為資料項構成的複合資料型別,所有表都習慣性的以 “_info” 結尾。

整個 class 檔案本質上就是一張表。

Class 檔案組成:

魔數

確定這個檔案是否能被虛擬機器接受,副檔名能隨便改動,檔案定製者可以隨意地選取一個魔數,只要選取沒有被廣泛應用的數字即可,Java 選的魔數是 0xCAFEBABE。

Class 檔案的版本

Class 檔案第 5,6 個位元組是次版本號,7,8 個位元組是主版本號,高版本 JDK 能相容以前版本的 Class 檔案,但不能執行以後版本的 Class 檔案,即使檔案內容沒有發生改變。Java 版本號從 45.0 開始,每增加一個版本號就加一。也就是 JDK 1.1 版本號 45.0-45.65535,JDK 1.2 版本號是 46.0-46.65535。

常量池

緊接著主版本號之後的就是常量池了,由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項 u2 型別的資料代表常量池容量計數值。這個值與 Java 其他數字不同,是由 1 開始的,0 表示沒有任何常量。對於其他集合型別,計數都是以 0 開始的。

當計數值為 22 時,代表有 21 項常量,索引為 1-21。

常量池主要存放字面量和符號引用。

字面量:類似於 Java 中的常量概念。

符號引用

類和介面的全限定名

欄位的名稱和描述符

方法的名稱和描述符

常量池中每一個常量都是一張表,這些表開頭都是一個 u1 型別的標誌位來代表當前的常量屬於哪種型別。

通過 u1 就能查到標誌位代表常量對應的表,就能通過表的結構來推算出常量。

訪問標誌

用於識別一些類或者介面層次的訪問資訊。包括這個是類還是介面,是否被宣告為 public final 等等。

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

類索引和父類索引都是一個 u2 型別的資料,而介面索引集合是一組 u2 型別的資料的集合。出了 Object 以外,所有的父類索引都不為 0 。介面索引用來描述類實現了哪些介面。介面索引第一個 u2 型別是介面計數器。通過索引查詢對應的常量池即可知道是哪些索引。

欄位表集合

欄位表用來描述介面或者類中宣告的變數。欄位包括類級變數和例項級變數,不包括方法中宣告的變數。

描述一個欄位需要

欄位作用域

是否 static

可變性

併發可見性

可否被序列化

欄位資料型別

欄位名稱

前面的性質是由布林值決定的,欄位名稱用常量池的常量修飾。

方法描述符描述了方法欄位型別,引數,返回值。

欄位表不會列出從超類或者父介面中繼承過來的欄位,但是可能會列出 Java 程式碼中不存在的欄位,比如內部類為了引用外部類會自動新增指向外部類例項的欄位。欄位無法過載。

方法表集合

返回值用一個大寫字母表示,陣列每一個維度用一個 [ 表示。引數在括號內,返回值在括號外。

方法 int indexOf(char[] source, int sourceOffset, char[] target) 的描述符為 ([CIS[C)I

要過載方法除了要和原方法具有相同的簡單名稱之外,還要有一個與原方法不同的特徵簽名。特徵簽名就是一個方法中各個引數在常量池符號引用的集合,也就是因為返回值不會包含在特徵簽名中,所以 Java 無法根據返回值不同來過載方法。

屬性表集合

在 Class,欄位表,方法表都可以攜帶自己的屬性表,用於描述某些場景專有的資訊。

屬性表不要求有嚴格的順序,並且編譯器可以實現不同的屬性,虛擬機器會忽略掉不認識的屬性。

屬性表結構

型別 名稱 數量
u2 arrtitube_name_index 1
u4 arrtibute_length 1
u1 info arrtibute_length

每一個屬性結構表是自定義的,只需要一個索引和一個 u4 型別說明屬性長度即可。

Code 屬性

Java 程式方法體中的程式碼經過 Javac 編譯後,最終變味位元組碼指令儲存在 Code 屬性內。

code屬性結構表

型別 名稱 數量
u2 arrtitube_name_index 1
u4 arrtibute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attribute_count 1
attribute_info attributes attribute_count

max_stack 表明了操作棧深度的最大值,在方法執行任意時刻,運算元棧都不會超過這個數。

max_locals 代表了局部變數所需的空間,這裡單位是 slot,一個 slot 儲存一個基本資料型別,long double 需要兩個 slot 儲存。Slot 是可以重複利用的,在作用域外的變數所佔用的 slot 可以被其他變數使用。

code 用於儲存位元組碼,每個指令是 u1 型別的單位元組。

code_length 雖然是 u4 但是虛擬機器規定一個方法不能超過 65535(u2長度) 條位元組碼指令。

一般很少會出現單個方法大於 65535 個位元組的情況,但是複雜的 JSP 或者 Android 程式碼到一定數量以後可能會出現這個情況,Android 只需要開啟 multiDexEnabled 即可。

// TODO 程式碼執行

Exceptions 屬性

Exceptions 指的方法描述時在 throws 關鍵字後列舉的異常。

LineNumberTable 屬性

用於描述原始碼行號和位元組碼行號的對應關係,不是執行時必要的屬性,可以通過

-g:none 或者 -g:lines 選項來取消或者要求生產它。取消的影響就是當丟擲異常的時候,堆疊中不會顯示出錯的行號,除錯的時候不能根據行號下斷點。

LocalVariableTable 屬性

用於描述區域性變數和 Java 原始碼中定義變數之間的關係,如果沒有生成這個屬性,所有的引數名稱會丟失, IDE 會自動用 arg0 等來代替引數。

SourceFile 屬性

用於記錄生成這個 Class 檔案的原始碼檔名稱。可以分別使用 Javac 的 -g:none 或 -g:source 選項來關閉或要求生成這項資訊。

ConstantValue 屬性

這個屬性的作用是通知虛擬機器自動為靜態變數賦值,只有被 static 關鍵字修飾的變數才能使用這項屬性。

###InnerClasses 屬性

用於記錄內類和宿主類之間的關聯,如果一個類定義了內部類,則編譯器會向它和它生成的內部類內新增這個屬性。

Deprecated 和 Synthetic 屬性

Deprecated 屬性表示某個類或者非法是否已經不推薦被使用。

Synthetic 用於區分這個類,欄位是編譯器生成的而不是通過 Java 程式碼翻譯得來的。

Signature 屬性

Signature 屬性用於記錄泛型資訊。Java 的泛型是由擦除實現的,但是無法將泛型與使用者自定義的普通型別同等對待,執行時做反射無法獲得資訊。現在 Java 執行時反射能得到泛型的資訊就是來自於這個屬性的。

BootstrapMethods 屬性

位元組碼指令

Class 檔案放棄了編譯後代碼的運算元長度對齊,意味著虛擬機器在解釋執行的時候會損失一些效能,但是可以省略很多填充和間隔符號,傳輸率提升。

由於限制了 Java 虛擬機器操作碼為一個位元組,所以如果給所有的虛擬機器相關資料都設計一條指令,會超出一個位元組能表示範圍了,所以 Java 指令集被設計為非完全獨立的。

在執行的時候 byte 和 short 會帶符號擴充套件為 int 型別,boolean 和 char 型別會零擴充套件為 int 型別。他們都用 int 型別作運算型別處理。

位元組碼指令看起來和彙編差不多,按照用途分為以下幾類

載入和儲存指令

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

這些指令包括

將一個區域性變數載入到操作棧

將一個數值從操作棧儲存到區域性變量表

將一個常量載入到運算元棧

擴充區域性變量表的訪問索引

運算指令

由於沒有直接支援 byte,short,char,boolean 型別的算數指令,對於這類資料的運算,應該使用 int 型別代替。

與彙編差不多,拿加法舉例,

iadd,ladd,fadd,dadd 分別代表 int 加法,long 加法,float 加法,double 加法

型別轉換指令

虛擬機器支援小範圍型別自動轉化為大範圍型別。

但是窄化型別轉換必須顯式使用轉換指令來完成。將 int 或者 long 轉換為整數型別 T 的時候,轉換過程只是丟棄最低位 N 個位元組,可能導致轉換結果和輸入值有不同的正負號。

long i = Long.MAX_VALUE;
int j = (int)i;
System.out.println(j);
// -1

浮點數轉換為整數的時候遵循以下規則:

  • 如果浮點值是 NaN 則轉為 0
  • 如果浮點值是無窮大,則轉為 int / long 能表示的最大值
  • 如果浮點數在表示範圍之內則截斷小數點

物件建立指令

虛擬機器中對於類例項和陣列的建立與操作都使用了不同的位元組碼指令。物件建立後可以通過物件訪問指令獲取物件例項中的元素。

建立類例項指令:new

建立陣列指令:newarray

訪問類欄位指令:getfield

把陣列元素載入到運算元棧指令:baload,caload

把一個運算元棧儲存到陣列元素:bastore

取陣列長度指令:arraylength

檢查例項型別指令:instanceof

運算元棧管理指令

將運算元棧一個或者兩個元素出棧:pop,pop2

複製棧頂元素:dup

交換棧頂兩個元素:swap

控制轉移指令

方法呼叫和返回指令

invokevirtual 指令用於呼叫物件的例項方法

invokeinterface 呼叫介面方法

invokespecial 呼叫一些需要特殊處理的例項方法,包括初始化方法和私有方法和父類方法

invokestatic 呼叫 static 方法

invokedynamic 指令用於在執行時動態解析出呼叫點限定符所引用方法

異常處理指令

在 Java 中顯式丟擲異常使用 athrow 指令來實現。

同步指令

Java 虛擬機器可以支援方法級別的同步和方法內部一段指令序列的同步,這兩種同步結構都是用管程實現的。

方法級的同步是隱式的,即無需通過位元組碼指令來控制,它實現在方法呼叫和返回操作之中。

虛擬機器可以從方法常量池中的 ACC_SYNCHRONIZED 訪問標誌得知一個方法是否宣告為同步方法。

當方法呼叫的時候,檢查這個值是否被設定了,如果設定了必須要得到管程然後才能執行方法。

同步一段指令集在 Java 中用的是 synchronized 實現的,Java 有 monitorenter 和 monitorexit 兩條指令來支援 synchronized 關鍵字的語意。正確實現 synchronized 關鍵字需要 javac 與 java 虛擬機器共同協作。

Java 虛擬機器必須能讀取 class 檔案並且精確實現包含在內其中的 Java 虛擬機器程式碼的語意。只要語意能夠保持完整,可以使用任何的方法去實現這些語意,虛擬機器如何處理 Class 檔案完全是實現者的事情,只要它在外部介面上和規範描述一致即可。

虛擬機器實現的方式主要有以下兩種

  • 將輸入的 Java 虛擬機器程式碼在載入或執行時翻譯成另外一種虛擬機器的指令集
  • 講輸入的 Java 虛擬機器程式碼在載入或者執行的時候直接翻譯成 CPU 本地指令集,即 JIT 技術