Java虛擬機JVM之類文件結構

一、JVM的“無關性”

JVM的無關性主要包括以下兩點:

  • 平臺無關性,任何平臺都能運行java代碼
  • 語言無關性:JVM能運行除JAVA以外的其他代碼

Java 源代碼首先需要使用 Javac 編譯器編譯成 .class 文件,然後由 JVM 執行 .class 文件,從而程序開始運行。JVM 只認識 .class 文件,它不關心是何種語言生成了 .class 文件,只要 .class 文件符合 JVM 的規範就能運行。 目前已經有 JRuby、Jython、Scala、Groovy 等語言能夠在 JVM 上運行。它們有各自的語法規則,不過它們的編譯器 都能將各自的源碼編譯成符合 JVM 規範的 .class 文件,從而能夠藉助 JVM 運行它們。

Java 語言中的各種變量、關鍵字和運算符號的語義最終都是由多條字節碼命令組合而成的, 因此字節碼命令所能提供的語義描述能力肯定會比 Java 語言本身更加強大。 因此,有一些 Java 語言本身無法有效支持的語言特性,不代表字節碼本身無法有效支持。

 

二、Class 文件結構

Class 文件是一組以 8 位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在 Class 文件中,中間沒有任何分隔符。Java 虛擬機規範規定 Class 文件採用一種類似 C 語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表,我們之後也主要對這兩種類型的數據類型進行解析。

  • 無符號數: 無符號數屬於基本數據類型,以 u1、u2、u4、u8 分別代表 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數,可以用它來描述數字、索引引用、數量值或 utf-8 編碼的字符串值。
  • 表: 表是由多個無符號數或其他表爲數據項構成的複合數據類型,名稱上都以 _info 結尾。

Class 文件具體由以下幾個構成:

  • 魔數
  • 版本信息
  • 常量池
  • 訪問標誌
  • 類索引、父類索引、接口索引集合
  • 字段表集合
  • 方法表集合
  • 屬性表集合

1、魔數

ass 文件的頭 4 個字節稱爲魔數,用來表示這個 Class 文件的類型。

Class 文件的魔數是用 16 進製表示的“CAFE BABE”,是不是很具有浪漫色彩?

魔數相當於文件後綴名,只不過後綴名容易被修改,不安全,因此在 Class 文件中標識文件類型比較合適。

2、版本信息

緊接着魔數的 4 個字節是版本信息,5-6 字節表示次版本號,7-8 字節表示主版本號,它們表示當前 Class 文件中使用的是哪個版本的 JDK。

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

3、常量池

版本信息之後從第 9 個字節開始,就是常量池的入口,常量池是 Class 文件中:

  • 與其他項目關聯最多的的數據類型;
  • 佔用 Class 文件空間最大的數據項目;
  • Class 文件中第一個出現的表類型數據項目。

常量池中存放兩種類型的常量:

  • 字面值常量

    字面值常量就是我們在程序中定義的字符串、被 final 修飾的值。

  • 符號引用

    符號引用就是我們定義的各種名字:類和接口的全限定名、字段的名字和描述符、方法的名字和描述符。

常量池中常量類型

類型 tag 描述 
CONSTANT_utf8_info 1 UTF-8編碼的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 5 長整型字面量
CONSTANT_Double_info 6 雙精度浮點型字面量
CONSTANT_Class_info 7 類或接口的符號引用
CONSTANT_String_info 8 字符串類型字面量
CONSTANT_Fieldref_info 9 字段的符號引用
CONSTANT_Methodref_info 10 類中方法的符號引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符號引用
CONSTANT_NameAndType_info 12 字段或方法的符號引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 標識方法類型
CONSTANT_InvokeDynamic_info 18 表示一個動態方法調用點

對於 CONSTANT_Class_info(此類型的常量代表一個類或者接口的符號引用),它的二維表結構如下:

類型 名稱 數量
u1 tag 1
u2 name_index 1

tag 是標誌位,用於區分常量類型;name_index 是一個索引值,它指向常量池中一個 CONSTANT_Utf8_info 類型常量,此常量代表這個類(或接口)的全限定名,這裏 name_index 值若爲 0x0002,也即是指向了常量池中的第二項常量。

CONSTANT_Utf8_info 型常量的結構如下:

類型 名稱 數量
u1 tag 1
u2 length 1
u1 bytes length

tag 是當前常量的類型;length 表示這個字符串的長度;bytes 是這個字符串的內容(採用縮略的 UTF8 編碼)

4、訪問標誌

在常量池結束之後,緊接着的兩個字節代表訪問標誌,這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個 Class 是類還是接口;是否定義爲 public 類型;是否被 abstract/final 修飾。

5、類索引、父類索引、接口索引集合

類索引和父類索引都是一個 u2 類型的數據,而接口索引集合是一組 u2 類型的數據的集合,Class 文件中由這三項數據來確定類的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。

由於 Java 不允許多重繼承,所以父類索引只有一個,除了 java.lang.Object 之外,所有的 Java 類都有父類,因此除了 java.lang.Object 外,所有 Java 類的父類索引都不爲 0。一個類可能實現了多個接口,因此用接口索引集合來描述。這個集合第一項爲 u2 類型的數據,表示索引表的容量,接下來就是接口的名字索引。

類索引和父類索引用兩個 u2 類型的索引值表示,它們各自指向一個類型爲 CONSTANT_Class_info 的類描述符常量,通過該常量總的索引值可以找到定義在 CONSTANT_Utf8_info 類型的常量中的全限定名字符串。

6、字段表集合

字段表集合存儲本類涉及到的成員變量,包括實例變量和類變量,但不包括方法中的局部變量。

每一個字段表只表示一個成員變量,本類中的所有成員變量構成了字段表集合。字段表結構如下:

類型 名稱 數量 說明
u2 access_flags 1 字段的訪問標誌,與類稍有不同
u2 name_index 1 字段名字的索引
u2 descriptor_index 1 描述符,用於描述字段的數據類型。 基本數據類型用大寫字母表示; 對象類型用“L 對象類型的全限定名”表示。
u2 attributes_count 1 屬性表集合的長度
u2 attributes attributes_count 屬性表集合,用於存放屬性的額外信息,如屬性的值。

字段表集合中不會出現從父類(或接口)中繼承而來的字段,但有可能出現原本 Java 代碼中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。

7、方法表集合

方法表結構與屬性表類似。

volatile 關鍵字 和 transient 關鍵字不能修飾方法,所以方法表的訪問標誌中沒有 ACC_VOLATILE 和 ACC_TRANSIENT 標誌。

方法表的屬性表集合中有一張 Code 屬性表,用於存儲當前方法經編譯器編譯後的字節碼指令。

8、屬性表集合

每個屬性對應一張屬性表,屬性表的結構如下:

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章