2012年7月4日 星期三

[ JVM 應用 ] Class 類別檔結構

前言 : 
程式編譯的結果從本地機器碼轉變為位元組碼, 是儲存格式的一小步, 卻是程式設計語言發展的一大步. Java 剛誕生時提出一個口號 "Write Once, Run Anywhere". 透過需將 .java 編譯成位元組編碼的 .class, 再透過不同平台的 JVM 載入與執行, 從而實現程式的 "一次編寫, 到處執行". 而各種不同平台的 JVM 與所有平台都統一使用的程式儲存格式 - 位元組編碼 (ByteCode) 是構成平臺無關性的基石. 


Class 類別檔的結構 : 
Class 檔是一組以 8 位元單位的二進位資料流, 各個資料項目嚴格按照順序緊湊的排列在 Class 檔中. 根據 Java JVM 規範, Class 檔案採用一種類似 C 語言結構體的伪結構來儲存, 這種伪結構中只有兩種資料類型 : 無符號數和表, 而無符號數屬於基本的資料結構, 以 u1, u2, u4, u8 來分別代表 1, 2, 4, 8 個位元組的無符號數. 

表是由多個無號數或其它表作為資料項目構成的複合型資料類型, 所有的表慣性會以 "_info" 結尾. 表用於描述有層次關係的複合結構的資料, 整個 Class 本質上就是一張表, 由下面的資料項目構成 : 
 

無論是無符號數或是表, 當需要描述同一類型但數量不定的多個資料時, 經常會使用一個前置的容量計數器加若干個連續資料項目的形式, 這時候稱一系列連續的某一類型資料為某一類型的集合. 在 上表中的資料項目, 無論是順序還是數量, 都是被嚴格限定的, 接下來我們來看看表中幾個資料項目的具體意義. 

Magic Number 與 Class 檔的版本 : 
每個 Class 檔的頭 4 個位元組稱為 Magic Number, 它的唯一作用是用於確定這個檔案是個合法可以被 JVM 接受的 Class 檔. 而 Class 文件 Magic Number 的值為 "0xCAFEBABE", 這個數值在 Java 還被稱作 "Oak" 語言時就已經確定下來. 緊接著 Magic Number 的 4 個位元組儲存的是 Class 檔的版本號 : 第 5 和 第 6 個位元組是次版本號 (Minor Version), 第 7 還有 第 8 個位元組是主版本 (Major Version). Java 的版本號是從 45 開始. JDK 1.1 後每個 JDK 大版本發佈主版本向上加 1 ( JDK 1.0~1.1 使用了 45.0 ~ 45.3 的版本號). 高版本的 JDK 能向下相容以前的版本 Class, 但不能執行以後版本的 Class 檔. 最新的 JDK 版本 1.7 產生的 Class 檔版本號值為 51.0. 接著為了講解, 我們用下面代碼編譯成的 Class 檔進行說明 : 
  1. package ch06;  
  2.   
  3. public class Ex6_1 {  
  4.     private int m;  
  5.       
  6.     public int inc(){return m+1;}  
  7. }  
下圖為使用Hex 編輯器打開上面編譯後的 Class 檔結果, 可以清楚看見頭 4 個位元組是 16 進制的 0xCAFEBABE, 代表次版本號的 0x0000 與 主版本號的 0x0032 (十進位的 50), 該版號說明這個 Class 檔是可以被 JDK 1.6 以上的 JVM 執行 : 
 

下表列出從 JDK 1.1 到 1.7 之間, 主流 JDK 編譯出來的預設與可支援的 Class 檔版本號 : 
 

常數池 : 
緊接著版本訊息之後是常數池入口, 常數池是 Class 檔結構中占用空間最大的資料項目之一. 由於常數池中常數的數量是不固定的, 所以在常數池的入口需要放置一項 u2 類型的資料, 代表常數池容量計數值 ( constant_pool_count ). 這個容量計數是從 1 而不是 0 開始, 參考下圖 : 
 

這代表常數池中有 21 項常數, 索引值為 1~22. 制定 Class 檔案格式規範時, 將第 0 項常數空出來是有特殊作用, 用途是為了滿足後面某些指向常數池的索引值的資料在特定情況下需要表達 "不引用任何一個常數池專案" 的意思, 這種情況就可以把索引值設為 0 來表示. Class 檔案結構中只有常數池的容量計數是從 1 開始, 對於其他集合類型, 包括介面索引, 欄位表集合等的容量計數都與一般習慣相同, 是從 0 開始. 常數池中主要存放兩大類常數 : 字面量 (Literal) 和 符號參考 (Symbolic References). 字面常量比較接近 Java 語言層面的常數概念, 如內容字串或被聲明為 final 的常數等. 而符號引用則屬於編譯原理的概念, 包括了下面三類常數 : 
* 類別和介面的全限定名 (Full Qualified Name) 
* 欄位的名稱和描述符 (Descriptor) 
* 方法的名稱 
Java 程式碼在進行 javac 編譯的時候並不像 C++ 那樣有 "link" 這一步驟, 而是 JVM 載入 Class 檔的時候進行動態的連接. 也就是說在 Class 檔中不會保留各個方法與欄位的最終記憶體布局資訊, 因此這些欄位與方法的符號參考不經過轉換是無法直接被 JVM 使用的. 當 JVM 執行時, 需要從常數池獲得對應的符號參考, 在類別新增或執行時解析並翻譯到具體的記憶體位址中.

常數池的每一項常數都是一個表, 共有 11 種結構. 這 11 種表都有一個共同特點, 就是表開始的第一位是一個 u1 類型的標誌位元 (tag, 取值為 1 至 12, 缺少標誌為 2 的資料類型), 代表當前這個常數屬於哪種常數類型, 11 種常數類型所代表的具體說明如下表 : 
 

之所以說常數池是最繁瑣的資料, 是因為這 11 種常數類型各自均有自己的結構. 回頭參考上面表到偏移位置 0x0000000A 是 0x07 可以知道這個常數屬於 CONSTANT_Class_info 類型 : 
 

此類型的常數代表一個類別或介面的符號參考, 它的結構如下說明 : 
 

tag 是標誌位元說明常數類型 ; name_index 是一個索引值, 它指向常數池中的一個 CONSTANT_Utf8_info 類型的常數, 此常數代表這個類 (或介面) 的全限定名, 這裡的 name_index (偏移位置 : 0x0000000B) 為 0x0002, 即指向常數池的第二項常數 : 
 

繼續從上表知道第二個常數類型 (偏移量 : 0x0000000D) 0x01 是 CONSTANT_Utf8_info 類型的常數, 而該類型結構如下表 : 
 

length 說明這個 UTF-8 編碼字串長度是多少位元組, 它後面緊接長度為 length 位元組的連續資料是一個使用 UTF-8 縮略編碼表示的字串. 因為 Class 檔中的方法, 欄位等都需要參考CONSTANT_Utf8_info 類型常數來描述名稱, 並且該類型最大長度也就是 Java 中方法和欄位名的最大長度 = 2^16 = 65535. 所以 Java 程式中如果定義了超過 64KB 英文字元的變數或方法, 將會編譯失敗. 底下為對應欄位標示 : 
 

到此為止, 我們分析了 Class 檔中 21 個常數中的前兩個, 其餘的 19 個常數都可以透過類似方法計算出來. 為了方便分析類別檔, 已經有一個現成的工具可以幫我們快速了解類別檔的資訊 : javap. 底下為其執行結果範例 : 

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...