字節(jié)碼文件的內(nèi)部結(jié)構(gòu)之謎

如果計(jì)算機(jī)的 CPU 只有「x86」這一種,或者操作系統(tǒng)只有 Windows 這一類,那么或許 Java 就不會誕生。Java 誕生之初就曾宣揚(yáng)過它的初衷,「一次編寫,多處運(yùn)行」,而它之所以能夠?qū)崿F(xiàn)跨平臺的一個(gè)核心點(diǎn)就在于,Java 引入「字節(jié)碼」屏蔽了與底層操作系統(tǒng)之間的差異

同一段 Java 程序在編譯后生成的字節(jié)碼文件是唯一的,不會因?yàn)槠脚_的不同而產(chǎn)生任何的變化。而同一段字節(jié)碼跑在不同實(shí)現(xiàn)的 JVM 上,會產(chǎn)生不同的機(jī)器指令。于底層而言,其實(shí) Sun 公司針對不同的操作系統(tǒng)開發(fā)了不同版本的 JVM,而這些 JVM 則通過識別上層的字節(jié)碼并向下解釋給操作系統(tǒng)執(zhí)行。因此,你的同一段字節(jié)碼在不同平臺下的 JVM 上運(yùn)行,會對應(yīng)到不同的機(jī)器指令,以此實(shí)現(xiàn)了跨平臺運(yùn)行。

而理解這個(gè)「字節(jié)碼」文件結(jié)構(gòu)就顯得十分重要了,理解它是如何存儲我們程序中的字段、方法、屬性、局部變量、各種常量值等等,是學(xué)習(xí)虛擬機(jī)工作原理的基礎(chǔ)。

那么,本文就來分析一下這個(gè)「字節(jié)碼」文件,解開它的神秘面紗。

Class 文件的總體概況

我們的 Java 文件被編譯器編譯成 Class 文件之后,整個(gè) Class 文件由若干個(gè) 0 和 1 組成為一個(gè)超長的「二進(jìn)制串」。各個(gè)項(xiàng)目按照嚴(yán)格的規(guī)范存儲并順序的排在一起,每個(gè)項(xiàng)目占幾個(gè)字節(jié)幾乎固定,所以 JVM 在解析的時(shí)候,只需要按照我們制定的規(guī)范一項(xiàng)一項(xiàng)的拆分解析即可。

整個(gè) Class 文件的各個(gè)項(xiàng)目以及它們之前的排列順序都是固定的,如圖:

image

其中 u2 表示當(dāng)前的項(xiàng)目總共占兩個(gè)字節(jié),當(dāng)然,u4 表示占四個(gè)字節(jié)。以 _info 結(jié)尾的項(xiàng)目表述為一張表,具體占多少字節(jié)數(shù)需要參見該表的內(nèi)部結(jié)構(gòu)。其實(shí),宏觀上來看,整個(gè) Class 文件也可以被看做是一張表。

魔數(shù)與 Class 文件的版本

Class 文件開頭的四個(gè)字節(jié)存儲的是當(dāng)前文件的「魔數(shù)」,所謂的「魔數(shù)」就是用于標(biāo)識當(dāng)前的文件是一個(gè)由 Java 文件編譯過來的 Class 文件。不是什么文件拿過來,我虛擬機(jī)都接受并運(yùn)行的,因?yàn)槲募臄U(kuò)展名是可以隨意更改的,所以有些文件可能就不是 Java 文件編譯而來的。

不同類型的文件有著不同的魔數(shù)值,圖片格式有圖片格式的的魔數(shù)值,視頻格式有視頻格式的魔數(shù)值,而我們 Class 文件的魔數(shù)值為:0xCAFEBABE 。我們使用 UltraEdit 任意打開一個(gè) Class 文件,會發(fā)現(xiàn)前四個(gè)字節(jié)都是一樣的。

image

參見 Class 文件的結(jié)構(gòu)圖,接下來的 minor_version 和 major_version 用于表述當(dāng)前 Class 文件的版本號。前者占兩個(gè)字節(jié),描述的是 Class 文件的「次版本號」,后者也占兩個(gè)字節(jié),描述的是 Class 文件的「主版本號」。

jdk1.1 之后的每個(gè)較大的版本都基于 jdk1.1 的主版本號加一,而 jdk1.1 的主版本號是從 45 開始的。所以,jdk1.2 的主版本號為 46,jdk1.3 的主版本號為 47 。當(dāng)然,對于每個(gè) jdk 版本中較小的變化而言,主版本號的值就不會發(fā)生變化,變化的是次版本號的值。

例如:jdk1.1.8 的版本號為 45.3,其中 45 是主版本號,3 是次版本號。

其實(shí),基本上 jdk1.2 以后的版本就只使用主版本號了,次版本號全為 0 。我電腦上的 jdk 版本是 1.8 的,于是得到它的版本號為 52(45+7) 。

image

那這個(gè)版本號有什么用呢?

虛擬機(jī)規(guī)范中指明,低版本 jdk 中的虛擬機(jī)不能運(yùn)行高版本的 Class 文件,而高版本 jdk 中的虛擬機(jī)則可以運(yùn)行低版本的 Class 文件。話可能有點(diǎn)繞,但主要意思就是,JVM 拒絕運(yùn)行比自己版本低的 Class 文件。

常量池

常量池算是類文件中比較繁瑣的一塊內(nèi)容了,在解析它之前我們先看一段 Java 代碼。

public class Person implements Serializable {
    private int num;
    private String name = "Yang";
    
    public void sayHello() {
        System.out.println("hello,my name is:" + this.name);
    }
}

這是一段再簡單不過的 Java 代碼,我們打開它編譯后的 Class 文件。

image

根據(jù)我們的 Class 文件格式,第 9,10 兩個(gè)字節(jié)表述 constant_pool_count,它代表了常量池中的容量。從圖中我們也可以看出來,constant_pool_count = 0x0035 = 53 。由于 Class 文件格式規(guī)定常量池中的項(xiàng)從 1 開始計(jì)數(shù),而不是從我們習(xí)慣的 0 開始的。所以整個(gè) Class 文件中共有 52([1,53)) 個(gè)常量項(xiàng),0 這個(gè)位置用于表述「不引用任何一個(gè)常量池項(xiàng)目」。

接下來的一項(xiàng),Class 文件格式中并沒有明確指明它總共占據(jù)多少個(gè)字節(jié),而只是聲明它是一張表。常量池中可以被定義的項(xiàng)目類型:

image

每一項(xiàng)又都是一張表,我們 52 個(gè)常量項(xiàng)就是這些項(xiàng)目的組合。因?yàn)槊總€(gè)常量項(xiàng)所對應(yīng)的表結(jié)構(gòu)都不盡相同,所每個(gè)常量項(xiàng)的表結(jié)構(gòu)中第一個(gè)字節(jié)存儲的就是一個(gè)標(biāo)志,用于區(qū)分當(dāng)前項(xiàng)的類型。例如:

image

這個(gè)值是 7,對應(yīng)的我們的常量項(xiàng)是 CONSTANT_Class_info。于是調(diào)來 CONSTANT_Class_info 表的結(jié)構(gòu):

image

CONSTANT_Class_info 總共占三個(gè)字節(jié),第一個(gè)字節(jié)存儲的標(biāo)志,不再多說。name_index 占兩個(gè)字節(jié),它是一個(gè)偏移地址,我們從上圖可以得到它的值是:0x0002,即它指向常量池中第二項(xiàng)常量。

我們?nèi)タ纯吹诙?xiàng)常量是什么,0x01 是它的標(biāo)志,表明它是 CONSTANT_Utf-8_info 類型的常量。

image

length 占兩個(gè)字節(jié),本例中的值為:0x0011 = 17 。所以該常量項(xiàng)還有 17 個(gè) bytes 存儲的是該常量的 utf-8 編碼值。可以看到:

image

這 17 個(gè)字節(jié)表述的 utf-8 字符串為:com/single/Person

我們手動的「翻譯」了常量池中前兩項(xiàng),其實(shí) Sun 公司為我們提供了工具幫我們計(jì)算字節(jié)碼文件中各個(gè)項(xiàng)目,這些工具都是非常好用的。

image

這里我們只分析了兩種常量項(xiàng)的表結(jié)構(gòu),其余 12 種大家可以自行搜索了解。我們常量池所有的常量都是有用的,Class 文件結(jié)構(gòu)中其他項(xiàng)目幾乎都會引用這里面的常量,待會再解釋。

訪問標(biāo)志

訪問標(biāo)志用于描述類文件的一些詳細(xì)信息,這個(gè) Class 是類還是接口,修飾為 public 或 protected,是否修飾為 final 等。Class 文件格式定義了訪問標(biāo)志占兩個(gè)字節(jié),總共 16 個(gè)比特位。

image

很簡單,一共 16 個(gè)比特位,這里只使用了 8 個(gè)比特位,如果最低位為 1 說明該 Class 被修飾為 public,為 0 則說明沒有被修飾為 public。一個(gè)標(biāo)志占了一個(gè)位,有兩個(gè)狀態(tài),1 為被修飾了某個(gè)狀態(tài),0 表示沒有被修飾為某個(gè)狀態(tài)。

例如:

0x0011(0000 0000 0001 0001):public + final

0x0201(0000 0010 0000 0001):public + 接口

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

這三個(gè)項(xiàng)目用于描述 Class 文件的繼承相關(guān)信息,它們按順序排列在訪問標(biāo)志后。根據(jù)我們的 Class 文件格式,this_class 占兩個(gè)字節(jié),存放的是相對于常量池的偏移值,同理 super_class 是其父類的符號引用。Java 除了 Object 類沒有父類,其他任何類都是有且僅有一個(gè)類,所以 Object 類的 super_class 的值為 0,表示未引用常量池中任何一項(xiàng)。

以我們上述的例子來說:

image

this_class 指向常量池中第一項(xiàng),super_class 指向常量池中第三項(xiàng)。通過查看常量池中的內(nèi)容,發(fā)現(xiàn)他們所對應(yīng)的常量項(xiàng)類型是 CONSTANT_Class_info ,繼續(xù)深入得到類的全限定名分別是:com/single/Person 和 java/lang/Object

接口項(xiàng)有稍許不同,因?yàn)?Java 中允許接口的多繼承,所以表述接口需要使用兩項(xiàng),interfaces_count 占兩個(gè)字節(jié),計(jì)數(shù)了 Class 文件實(shí)現(xiàn)的接口數(shù)量,interfaces 占兩個(gè)字節(jié),存儲的是相對于常量池的偏移值。

這里,interfaces_count 的值為:0x0001 ,interfaces 的值為:0x0005。于是得到該 Class 文件所實(shí)現(xiàn)的接口的名稱為:java/io/Serializable

字段表集合

字段其實(shí)就是接口或者類中定義的變量,有實(shí)例變量和類變量之分。當(dāng)然,方法中定義的局部變量肯定不能算字段的,字段特指那些定義在方法之外,類或接口之中的變量。

每個(gè)字段表只能描述一個(gè)字段的信息,一個(gè) Class 文件中往往又有多個(gè)字段,所以 Class 文件格式在字段表之前定義了兩個(gè)字節(jié)的項(xiàng) fields_count 來計(jì)數(shù)字段的數(shù)量。

字段表的標(biāo)準(zhǔn)結(jié)構(gòu)如下:

image

access_flags 占兩個(gè)字節(jié),它描述了該字段的基本訪問標(biāo)志,主要包括:字段的作用域,實(shí)例或類變量(static),可否序列化(transient),可變性(final)等等。這個(gè)屬性的存儲形式和我們之前介紹的類的訪問標(biāo)識存儲的思想是類似的,每種狀態(tài)使用一個(gè)比特位來標(biāo)識對于該狀態(tài)的修飾與否。

image

參見我們上述的例子:

image

第一個(gè) 0x0002 表示字段表數(shù)量為 2,即當(dāng)前 Class 文件中有兩個(gè)字段。第二個(gè) 0x0002 表示當(dāng)前字段被 「private」 關(guān)鍵字修飾。

我們接著看這個(gè)字段表。

name_index 占兩個(gè)字節(jié),它存儲的是當(dāng)前字段的名稱在常量池中的偏移量值。

descriptor_index 占兩個(gè)字節(jié),它是對當(dāng)前字段基本數(shù)據(jù)類型的描述,存儲的也是一個(gè)字符常量在常量池中的偏移值。但是你如果對應(yīng)到常量池中去看的話,你會發(fā)現(xiàn)這個(gè)描述符的的值是: I

image

基本數(shù)據(jù)類型與實(shí)際存儲的符號之間有這么一種映射關(guān)系,為的是簡單存儲。其中,如果字段是數(shù)組類型的話,需要前置一個(gè) 『 』,多維數(shù)組就前置多個(gè)該符號進(jìn)行描述。

接著看字段表。

接下來的 attributes_count 和 attributes 描述的是當(dāng)前字段的「屬性」。所謂「屬性」也即字段的額外信息描述。我們的第一個(gè)字段沒有額外的屬性,所以 attributes_count 為 0 。

下面我們完整分析一下第二個(gè)字段的字節(jié)碼:

image

access_flags 的值為 0x0002,對應(yīng)的訪問修飾符是:private 。name_index 的值對應(yīng)于字段名稱在常量池中的偏移值。

image

descriptor_index 的值為:0x000A ,對應(yīng)的常量值是:Ljava/lang/String 。同樣,它也沒有屬性描述。

方法表集合

理解了字段表,方法表的內(nèi)容就很容易理解了。下面是方法表的標(biāo)準(zhǔn)結(jié)構(gòu):

image

針對我們上述的示例,簡單分析一下:

image

首先,0x0002 表示整個(gè) Class 文件中有兩個(gè)方法(一個(gè)是我們自己編寫的 sayHello 方法,還有一個(gè)是編譯器增加的實(shí)例構(gòu)造器《init》方法)。

然后,0x0001 指明了該方法的訪問標(biāo)志:public,0x000B 指明了該方法名稱在常量池中的偏移值,對應(yīng)到常量池中的常量:<init>

接下來是這個(gè) descriptor_index,字段表中該屬性存儲的是字段的數(shù)據(jù)類型,而在方法表中,這個(gè)屬性存儲的「東西」要稍微多一些,它存儲了方法的參數(shù)個(gè)數(shù),參數(shù)類型,返回值等信息。例如我們此示例中,descriptor_index 對應(yīng)于常量池中的常量:()V(0x000C)。

當(dāng)然,這個(gè)方法比較簡單,沒有參數(shù),返回值類型為 void。我們再看一個(gè)稍微復(fù)雜點(diǎn)的例子:

public int executeNum(int a,String b,char[] x)

對應(yīng)的精簡版存儲形式:

(IL/java/lang/String[C)I

接著就是屬性表,顯然從我們的字節(jié)碼表中可以看出來,attributes_count 的值為 1,說明該方法存在一個(gè)屬性,下面我們來看看屬性表有哪些嚴(yán)格的「約束」。

虛擬機(jī)規(guī)范中定義的屬性有很多,并且每種屬性都有不同于其他屬性的表結(jié)構(gòu),但是所有的屬性都必須包含以下三個(gè)項(xiàng)。

image

通過前兩個(gè)字節(jié)可以辨別當(dāng)前的屬性類型。于我們這里的示例而言,attrubute_name_index 的值為 0x000D(Code),所以虛擬機(jī)可以調(diào)來 Code 表結(jié)構(gòu)繼續(xù)完成解析,Code 表結(jié)構(gòu)如下:

image

接著分析

然后的四個(gè)字節(jié)表明該屬性所占用的總字節(jié)數(shù),attribute_length 等于 0x0000003D(61),然后一步一步分析即可,我們這里不再繼續(xù)分析了。其實(shí) Code 屬性表最主要的一個(gè)作用是,存儲當(dāng)前方法在編譯后所生成的所有字節(jié)碼指令,并記錄所需局部變量表的大小等有關(guān)方法運(yùn)行的信息。

還有一些其他屬性表我們這里為了不使篇幅過長,將在后續(xù)文章中繼續(xù)分析。

總體上而言,所謂的字節(jié)碼文件,或者說 Class 文件就是編譯器嚴(yán)格按照虛擬機(jī)規(guī)范生成的一串二進(jìn)制,虛擬機(jī)在進(jìn)行解析的時(shí)候也是嚴(yán)格按照虛擬機(jī)規(guī)范進(jìn)行解析,這樣就使得 Class 文件中所有的信息都能夠被虛擬機(jī)讀取解析。


文章中的所有代碼、圖片、文件都云存儲在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關(guān)注微信公眾號:撲在代碼上的高爾基,所有文章都將同步在公眾號上。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容