JVM 的“無關性”
談論 JVM 的無關性,主要有以下兩個:
- 平台無關性:任何作業系統都能運作 Java 代碼
- 語言無關性: JVM 能運作除 Java 以外的其他代碼
Java 源代碼首先需要使用 Javac 編譯器編譯成 .class 檔案,然後由 JVM 執行 .class 檔案,進而程式開始運作。
JVM 隻認識 .class 檔案,它不關心是何種語言生成了 .class 檔案,隻要 .class 檔案符合 JVM 的規範就能運作。 目前已經有 JRuby、Jython、Scala 等語言能夠在 JVM 上運作。它們有各自的文法規則,不過它們的編譯器 都能将各自的源碼編譯成符合 JVM 規範的 .class 檔案,進而能夠借助 JVM 運作它們。
Java 語言中的各種變量、關鍵字和運算符号的語義最終都是由多條位元組碼指令組合而成的, 是以位元組碼指令所能提供的語義描述能力肯定會比 Java 語言本身更加強大。 是以,有一些 Java 語言本身無法有效支援的語言特性,不代表位元組碼本身無法有效支援。
Class 檔案結構
Class 檔案時二進制檔案,它的内容具有嚴格的規範,檔案中沒有任何空格,全都是連續的 0/1。Class 檔案 中的所有内容被分為兩種類型:無符号數、表。
- 無符号數 無符号數表示 Class 檔案中的值,這些值沒有任何類型,但有不同的長度。u1、u2、u4、u8 分别代表 1/2/4/8 位元組的無符号數。
- 表 由多個無符号數或者其他表作為資料項構成的符合資料類型。
Class 檔案具體由以下幾個構成:
- 魔數
- 版本資訊
- 常量池
- 通路标志
- 類索引、父類索引、接口索引集合
- 字段表集合
- 方法表集合
- 屬性表集合
Class 檔案的頭 4 個位元組稱為魔數,用來表示這個 Class 檔案的類型。
Class 檔案的魔數是用 16 進制表示的“CAFE BABE”,是不是很具有浪漫色彩?
魔數相當于檔案字尾名,隻不過字尾名容易被修改,不安全,是以在 Class 檔案中辨別檔案類型比較合适。
緊接着魔數的 4 個位元組是版本資訊,5-6 位元組表示次版本号,7-8 位元組表示主版本号,它們表示目前 Class 檔案中使用的是哪個版本的 JDK。
高版本的 JDK 能向下相容以前版本的 Class 檔案,但不能運作以後版本的 Class 檔案,即時檔案格式并未發生任何變化,虛拟機也必需拒絕執行超過其版本号的 Class 檔案。
版本資訊之後就是常量池,常量池中存放兩種類型的常量:
-
字面值常量
字面值常量就是我們在程式中定義的字元串、被 final 修飾的值。
-
符号引用
符号引用就是我們定義的各種名字:類和接口的全限定名、字段的名字和描述符、方法的名字和描述符。
常量池的特點
- 常量池中常量數量不固定,是以常量池開頭放置一個 u2 類型的無符号數,用來存儲目前常量池的容量。
- 常量池的每一項常量都是一個表,表開始的第一位是一個 u1 類型的标志位(tag),代表目前這個常量屬于哪種常量類型。
常量池中常量類型
類型 | 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 | ||
u2 | name_index |
tag 是标志位,用于區分常量類型;name_index 是一個索引值,它指向常量池中一個 CONSTANT_Utf8_info 類型常量,此常量代表這個類(或接口)的全限定名,這裡 name_index 值若為 0x0002,也即是指向了常量池中的第二項常量。
CONSTANT_Utf8_info 型常量的結構如下:
length | ||
bytes |
tag 是目前常量的類型;length 表示這個字元串的長度;bytes 是這個字元串的内容(采用縮略的 UTF8 編碼)
在常量池結束之後,緊接着的兩個位元組代表通路标志,這個标志用于識别一些類或者接口層次的通路資訊,包括:這個 Class 是類還是接口;是否定義為 public 類型;是否被 abstract/final 修飾。
類索引和父類索引都是一個 u2 類型的資料,而接口索引集合是一組 u2 類型的資料的集合,Class 檔案中由這三項資料來确定類的繼承關系。類索引用于确定這個類的全限定名,父類索引用于确定這個類的父類的全限定名。
由于 Java 不允許多重繼承,是以父類索引隻有一個,除了 java.lang.Object 之外,所有的 Java 類都有父類,是以除了 java.lang.Object 外,所有 Java 類的父類索引都不為 0。一個類可能實作了多個接口,是以用接口索引集合來描述。這個集合第一項為 u2 類型的資料,表示索引表的容量,接下來就是接口的名字索引。
類索引和父類索引用兩個 u2 類型的索引值表示,它們各自指向一個類型為 CONSTANT_Class_info 的類描述符常量,通過該常量總的索引值可以找到定義在 CONSTANT_Utf8_info 類型的常量中的全限定名字元串。
字段表集合存儲本類涉及到的成員變量,包括執行個體變量和類變量,但不包括方法中的局部變量。
每一個字段表隻表示一個成員變量,本類中的所有成員變量構成了字段表集合。字段表結構如下:
說明 | |||
---|---|---|---|
access_flags | 字段的通路标志,與類稍有不同 | ||
字段名字的索引 | |||
descriptor_index | 描述符,用于描述字段的資料類型。 基本資料類型用大寫字母表示; 對象類型用“L 對象類型的全限定名”表示。 | ||
attributes_count | 屬性表集合的長度 | ||
attributes | 屬性表集合,用于存放屬性的額外資訊,如屬性的值。 |
字段表集合中不會出現從父類(或接口)中繼承而來的字段,但有可能出現原本 Java 代碼中不存在的字段,譬如在内部類中為了保持對外部類的通路性,會自動添加指向外部類執行個體的字段。
方法表結構與屬性表類似。
attribute_name_index | ||
u4 | attribute_length | |
info |