
人生最苦痛的是夢(mèng)醒了無路可以走,自由固不是錢所能買到的,但能夠?yàn)殄X而賣掉。
之前在閱讀 ASM 文檔時(shí),對(duì)于已編譯類的結(jié)構(gòu)、方法描述符、訪問標(biāo)志、ACC_PUBLIC、ACC_PRIVATE、各種字節(jié)碼指令等等許多概念聽起來都是云山霧罩、一知半解,原因就在于對(duì)類文件結(jié)構(gòu)和類加載機(jī)制不夠了解。直到后來細(xì)讀了《深入理解 Java 虛擬機(jī)》中虛擬機(jī)執(zhí)行子系統(tǒng)的相關(guān)內(nèi)容,才建立了清晰的認(rèn)知。如果你也和我一樣,不了解類結(jié)構(gòu)和類加載,但是工作中又涉及到字節(jié)碼相關(guān)內(nèi)容,相信后面兩篇文章會(huì)對(duì)你有所幫助。
我們所編寫的每一行代碼,要在機(jī)器上運(yùn)行最終都需要編譯成二進(jìn)制的機(jī)器碼 CPU 才能識(shí)別。但是由于虛擬機(jī)的存在,屏蔽了操作系統(tǒng)與 CPU 指令集的差異性,類似于 Java 這種建立在虛擬機(jī)之上的編程語言通常會(huì)編譯成一種中間格式的文件來存儲(chǔ),比如我們今天要聊的字節(jié)碼(ByteCode)文件。
一. 語言無關(guān)性
Java 虛擬機(jī)的設(shè)計(jì)者在設(shè)計(jì)之初就考慮并實(shí)現(xiàn)了其它語言在 Java 虛擬機(jī)上運(yùn)行的可能性。所以并不是只有 Java 語言能夠跑在 Java 虛擬機(jī)上,時(shí)至今日諸如 Kotlin、Groovy、Jython、JRuby 等一大批 JVM 語言都能夠在 Java 虛擬機(jī)上運(yùn)行。它們和 Java 語言一樣都會(huì)被編譯器編譯成字節(jié)碼文件,然后由虛擬機(jī)來執(zhí)行。所以說類文件(字節(jié)碼文件)具有語言無關(guān)性。
二. Class 文件結(jié)構(gòu)
Class 文件是一組以 8 位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)嚴(yán)格按照順序緊湊的排列在 Class 文件中,中間無任何分隔符,這使得整個(gè) Class 文件中存儲(chǔ)的內(nèi)容幾乎全部都是程序運(yùn)行的必要數(shù)據(jù),沒有空隙存在。當(dāng)遇到需要占用 8 位字節(jié)以上空間的數(shù)據(jù)項(xiàng)時(shí),會(huì)按照高位在前的方式分割成若干個(gè) 8 位字節(jié)進(jìn)行存儲(chǔ)。
Java 虛擬機(jī)規(guī)范規(guī)定 Class 文件格式采用一種類似與 C 語言結(jié)構(gòu)體的微結(jié)構(gòu)體來存儲(chǔ)數(shù)據(jù),這種偽結(jié)構(gòu)體中只有兩種數(shù)據(jù)類型:無符號(hào)數(shù)和表。
- 無符號(hào)數(shù)屬于基本的數(shù)據(jù)類型,以 u1、u2、u4、u8來分別代表 1 個(gè)字節(jié)、2 個(gè)字節(jié)、4 個(gè)字節(jié)和 8 個(gè)字節(jié)的無符號(hào)數(shù),無符號(hào)數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值或者按照 UTF-8 編碼結(jié)構(gòu)構(gòu)成的字符串值。
- 表是由多個(gè)無符號(hào)數(shù)或者其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型,所有表都習(xí)慣性地以「_info」結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個(gè) Class 文件就是一張表,它由下表中所示的數(shù)據(jù)項(xiàng)構(gòu)成。
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| 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 文件中存儲(chǔ)的字節(jié)嚴(yán)格按照上表中的順序緊湊的排列在一起。哪個(gè)字節(jié)代表什么含義,長(zhǎng)度是多少,先后順序如何都是被嚴(yán)格限制的,不允許有任何改變。
2.1 魔數(shù)與 Class 文件版本
每個(gè) Class 文件的頭 4 個(gè)字節(jié)稱為魔數(shù)(Magic Number),它的唯一作用是確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)接收的 Calss 文件。之所以使用魔數(shù)而不是文件后綴名來進(jìn)行識(shí)別主要是基于安全性的考慮,因?yàn)槲募缶Y名是可以隨意更改的。Class 文件的魔數(shù)值為「0xCAFEBABE」。
緊接著魔數(shù)的 4 個(gè)字節(jié)存儲(chǔ)的是 Class 文件的版本號(hào):第 5 和第 6 兩個(gè)字節(jié)是次版本號(hào)(Minor Version),第 7 和第 8 個(gè)字節(jié)是主版本號(hào)(Major Version)。高版本的 JDK 能夠向下兼容低版本的 Class 文件,虛擬機(jī)會(huì)拒絕執(zhí)行超過其版本號(hào)的 Class 文件。
2.2 常量池
主版本號(hào)之后是常量池入口,常量池可以理解為 Class 文件之中的資源倉庫,它是 Class 文件結(jié)構(gòu)中與其他項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類型,也是占用 Class 文件空間最大的數(shù)據(jù)項(xiàng)目之一,同是它還是 Class 文件中第一個(gè)出現(xiàn)的表類型數(shù)據(jù)項(xiàng)目。
因?yàn)槌A砍刂谐A康臄?shù)量是不固定的,所以在常量池入口需要放置一個(gè) u2 類型的數(shù)據(jù)來表示常量池的容量「constant_pool_count」,和計(jì)算機(jī)科學(xué)中計(jì)數(shù)的方法不一樣,這個(gè)容量是從 1 開始而不是從 0 開始計(jì)數(shù)。之所以將第 0 項(xiàng)常量空出來是為了滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)「不引用任何一個(gè)常量池項(xiàng)目」的含義,這種情況可以把索引值置為 0 來表示。
Class 文件結(jié)構(gòu)中只有常量池的容量計(jì)數(shù)是從 1 開始的,其它集合類型,包括接口索引集合、字段表集合、方法表集合等容量計(jì)數(shù)都是從 0 開始。
常量池中主要存放兩大類常量:字面量和符號(hào)引用。
- 字面量比較接近 Java 語言層面的常量概念,如字符串、聲明為 final 的常量值等。
-
符號(hào)引用屬于編譯原理方面的概念,包括了以下三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
2.3 訪問標(biāo)志
緊接著常量池之后的兩個(gè)字節(jié)代表訪問標(biāo)志(access_flag),這個(gè)標(biāo)志用于識(shí)別一些類或者接口層次的訪問信息,包括這個(gè) Class 是類還是接口;是否定義為 public 類型;是否定義為 abstract 類型;如果是類的話,是否被申明為 final 等。具體的標(biāo)志位以及標(biāo)志的含義見下表:
| 標(biāo)志名稱 | 標(biāo)志值 | 含義 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 是否為 public 類型 |
| ACC_FINAL | 0x0010 | 是否被聲明為 final,只有類可設(shè)置 |
| ACC_SUPER | 0x0020 | 是否允許使用 invokespecial 字節(jié)碼指令的新語意,invokespecial 指令的語意在 JKD 1.0.2 中發(fā)生過改變,微聊區(qū)別這條指令使用哪種語意,JDK 1.0.2 編譯出來的類的這個(gè)標(biāo)志都必須為真 |
| ACC_INTERFACE | 0x0200 | 標(biāo)識(shí)這是一個(gè)接口 |
| ACC_ABSTRACT | 0x0400 | 是否為 abstract 類型,對(duì)于接口或者抽象類來說,此標(biāo)志值為真,其它類值為假 |
| ACC_SYNTHETIC | 0x1000 | 標(biāo)識(shí)這個(gè)類并非由用戶代碼產(chǎn)生 |
| ACC_ANNOTATION | 0x2000 | 標(biāo)識(shí)這是一個(gè)注解 |
| ACC_ENUM | 0x4000 | 標(biāo)識(shí)這是一個(gè)枚舉 |
access_flags 中一共有 16 個(gè)標(biāo)志位可以使用,當(dāng)前只定義了其中的 8 個(gè),沒有使用到的標(biāo)志位要求一律為 0。
2.4 類索引、父類索引與接口索引集合
類索引(this_class)和父類索引(super_class)都是一個(gè) u2 類型的數(shù)據(jù),而接口索引集合(interfaces)是一組 u2 類型的數(shù)據(jù)集合,Class 文件中由這三項(xiàng)數(shù)據(jù)來確定這個(gè)類的繼承關(guān)系。
- 類索引用于確定這個(gè)類的全限定名
- 父類索引用于確定這個(gè)類的父類的全限定名
- 接口索引集合用于描述這個(gè)類實(shí)現(xiàn)了哪些接口
2.5 字段表集合
字段表集合(field_info)用于描述接口或者類中聲明的變量。字段(field)包括類變量和實(shí)例變量,但不包括方法內(nèi)部聲明的局部變量。下面我們看看字段表的結(jié)構(gòu):
| 類型 | 名稱 | 數(shù)量 |
|---|---|---|
| u2 | access_flag | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
字段修飾符放在 access_flags 中,它與類中的 access_flag 非常相似,都是一個(gè) u2 的數(shù)據(jù)類型。
| 標(biāo)志名稱 | 標(biāo)志值 | 含義 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 字段是否為 public |
| ACC_PRIVATE | 0x0002 | 字段是否為 private |
| ACC_PROTECTED | 0x0004 | 字段是否為 protected |
| ACC_STATIC | 0x0008 | 字段是否為 static |
| ACC_FINAL | 0x0010 | 字段是否為 final |
| ACC_VOLATILE | 0x0040 | 字段是否為 volatile |
| ACC_TRANSIENT | 0x0080 | 字段是否為 transient |
| ACC_SYNTHETIC | 0x1000 | 字段是否由編譯器自動(dòng)生成 |
| ACC_ENUM | 0x4000 | 字段是否為 enum |
2.6 方法表集合
Class 文件中對(duì)方法的描述和對(duì)字段的描述是完全一致的,方法表中的結(jié)構(gòu)和字段表的結(jié)構(gòu)一樣。
因?yàn)?volatile 關(guān)鍵字和 transient 關(guān)鍵字不能修飾方法,所以方法表的訪問標(biāo)志中沒有 ACC_VOLATILE 和 ACC_TRANSIENT。與之相對(duì)的,synchronizes、native、strictfp 和 abstract 關(guān)鍵字可以修飾方法,所以方法表的訪問標(biāo)志中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 標(biāo)志。
對(duì)于方法里的代碼,經(jīng)過編譯器編譯成字節(jié)碼指令后,存放在方法屬性表中一個(gè)名為「Code」的屬性里面。
2.7 屬性表集合
在 Class 文件、字段表、方法表中都可以攜帶自己的屬性表(attribute_info)集合,用于描述某些場(chǎng)景專有的信息。
屬性表集合不像 Class 文件中的其它數(shù)據(jù)項(xiàng)要求這么嚴(yán)格,不強(qiáng)制要求各屬性表的順序,并且只要不與已有屬性名重復(fù),任何人實(shí)現(xiàn)的編譯器都可以向?qū)傩员碇袑懭胱约憾x的屬性信息,Java 虛擬機(jī)在運(yùn)行時(shí)會(huì)略掉它不認(rèn)識(shí)的屬性。
寫在最后
為了控制篇幅,這篇文章里丟棄了很多細(xì)節(jié),比如常量池的項(xiàng)目類型、方法表、屬性表的具體內(nèi)容等等。建議想要深入了解的同學(xué)可以自己動(dòng)手將 Java 類編譯成二進(jìn)制字節(jié)碼文件,根據(jù)文章里介紹的類文件結(jié)構(gòu)逐個(gè)字符去對(duì)照和實(shí)驗(yàn),有助于加深理解。
關(guān)于「類文件結(jié)構(gòu)」我們就介紹到這里,下一篇我們來聊聊「虛擬機(jī)的類加載機(jī)制」。

