走進 Class 文件
本文是作者原創(chuàng)文章,如需轉(zhuǎn)載奴璃,請先聯(lián)系
首先發(fā)布于我的個人網(wǎng)站
Reference
- <<深入理解 Java 虛擬機>> 周志明著
- << JVM Specification Java SE 1.8 >>
作為一名 Java 程序員厅缺,相信大家都知道方咆,我們寫的 Java 源碼會被編譯成 .class
文件然后被 JVM
加載運行。為了深入地學(xué)習(xí) JVM
我們很有必要知道 .class
文件的結(jié)構(gòu)蜂挪,它們是怎么被加載進入 JVM
滓走,以及如何被 JVM
解析的垦江。
先看一個例子,假設(shè)我們有如下的源碼:
public class Example {
private int m;
public int inc() {
return m + 1;
}
public static void main(String[] args) {
System.out.println("hello world");
}
}
打開命令行闲坎,編譯并且運行一下這個例子的源碼:
這么看疫粥,并沒有什么特別的,僅僅是一個最最最簡單的 “hello world” 程序腰懂。但是這次我們的關(guān)注點不放在這段程序運行的結(jié)果上梗逮,我們的關(guān)注點而是我們使用 javac
命令編譯源碼后產(chǎn)生的 Example.class
文件。
Example.class
文件里面包含的就是我們所說的“字節(jié)碼(byte code)”绣溜,讓我們打開這個文件看看慷彤,由于是字節(jié)碼,我們要使用能夠識別 hex
的工具打開:
// open with vim
vim Example.class
// inside vim type
: % ! xxd
// want to go back
: % ! xxd -r
當(dāng)你輸入 vim Example.class
的時候怖喻,你應(yīng)該會看到一堆亂碼底哗, 然后當(dāng)你輸入 : % ! xxd
之后,你應(yīng)該能夠看到像下面圖片一樣的內(nèi)容:
一個小小的 “hello world” 程序的字節(jié)碼就像上面顯示的一樣(看起來好復(fù)雜啊 :< )锚沸,不用方跋选!下面我們將詳細的來解釋一下這個字節(jié)碼文件的結(jié)構(gòu)是怎樣的。
Class 文件結(jié)構(gòu)
概述
Class 文件是一組以 8 位字節(jié)碼為基礎(chǔ)單位的二進制流哗蜈,各個數(shù)據(jù)項目嚴格按照規(guī)定的順序緊湊地排列在 Class 文件中前标,中間沒有任何分隔符。當(dāng)遇到需要占用 8 位以上的空間時距潘,按照高位在前的方式分割成若干個 8 位字節(jié)進行存儲炼列。
Java 虛擬機規(guī)范規(guī)定,Class 文件采用一種偽結(jié)構(gòu)體來存儲數(shù)據(jù)音比,這種偽結(jié)構(gòu)體里只有兩種數(shù)據(jù)類型:
- 無符號數(shù) (u1, u2, u4, u8)
- 表 (_info)
下圖顯示了一個完整的 Class
文件的結(jié)構(gòu)俭尖,后文將逐一介紹這幾個部分。
魔數(shù)與版本號
Class
文件中的所有數(shù)據(jù)都是按照嚴格規(guī)定的順序進行排列的,“魔數(shù)” 就是文件中出現(xiàn)的第一個數(shù)據(jù)項稽犁。所有由 javac
命令編譯生成的 .calss
文件前四個字節(jié)都是一樣的焰望,稱為“魔數(shù) (magic number)”,內(nèi)容為 CAFEBABE
缭付。
Class
文件的版本號分為兩個部分:
-
Minor Version
柿估,第 5 和 第 6 字節(jié)循未; -
Major Version
陷猫,第 7 和 第 8 字節(jié);
上圖 major version
中的 34 是十六進制的妖,它代表十進制中的 52绣檬,下表為 Java Version
和 Major Version
的對照表,可以看到 52 對應(yīng)的是 Java 8
嫂粟。
下面的是我機器上的 Java
版本娇未,可以看到是 Java 1.8.0_51
和上面的結(jié)果符合。
常量池
常量池 (constant pool) 是 Class
文件里比較復(fù)雜的一個數(shù)據(jù)項星虹。顧名思義零抬,常量池是一個用來堆放常量的地方,和 “線程池” 的概念基本“類”同宽涌。由于每個程序里面需要的常量數(shù)目都相同平夜,所以常量池的長度也是不確定的。所以卸亮,我們需要一個 length
變量來指示當(dāng)前常量池的大小忽妒,這個變量的名字叫做 “常量池計數(shù)器 (constant_pool_count
)”。值得一提的是兼贸,這個計數(shù)器是從 1
開始計數(shù)的(0 保留下來做別的用處了)段直,不像計算機科學(xué)里的大多數(shù)計數(shù)器從零開始,這也是 Class
文件里唯一一個特例溶诞。
接著 “常量池計數(shù)器” 后面就是 “常量表(cp_info)” 了鸯檬,這個表里的每一個子項 (entry) 都是一個表。每一個子項都必定是已經(jīng)規(guī)定好的 14 個螺垢,下圖列出了這 14 種可能性喧务,以及每個字表的結(jié)構(gòu) (這 14 個字表的結(jié)構(gòu)都不一樣)。
常量池中主要放置兩種類型的常量:
- 字面量 (Literal)
- 符號引用 (Symbol Reference)
下面甩苛,我們嘗試著分析一下我們一開始的例子里面的常量池數(shù)據(jù)蹂楣。從下圖中我們可以看到,計數(shù)器的數(shù)值轉(zhuǎn)化成十進制后是 35讯蒲。這表明一共有 34 項常量 1 ~ 34
痊土,零保留下來了,表示 “不引用常量池的任何一個項目”墨林。
緊接著計數(shù)器的是一個 tag 0x0a
-> 10 (十進制)赁酝,查上表犯祠,可以知道這是一個 CONSTANT_Methodref_info
類型的表 (符號引用)。再仔細看表 CONSTANT_Methodref_info
由三個子項組成酌呆,分別是 tag (u1 類型)衡载,index(u2 類型,指向方法屬于的類)隙袁,index(u2 類型痰娱,指向方法的名稱)。按照數(shù)據(jù)類型菩收,截取相應(yīng)的數(shù)據(jù):
- tag -> 0x0a (10)
- index -> 0007 (7)
- index -> 0014 (20)
查表可以知道梨睁,這個方法引用符指向的是 java/lang/Object."<init>":()V
。乍一看我們可以想象到娜饵,這個東西 “像” 是 Object
類的構(gòu)造方法坡贺。其實 Java
為我們提供了一個字節(jié)碼分析工具,我們可以輸入如下命令:
javap -verbose Example
可以看到輸入如下 (和我們分析的吻合):
后文不再逐一分析常量池的內(nèi)容箱舞,有需要可以自行驗證多幾項遍坟。總而言之晴股,常量池里的內(nèi)容如果是字面量就可以被別的表引用愿伴,如果是符號引用則會繼續(xù)引用別的表。多一句嘴队魏,這寫符號引用會在某個階段被替換成內(nèi)存中的地址公般,在傳統(tǒng)的編譯型語言中 (比如 C/C++),這一步驟被稱為 “鏈接 (linking)”胡桨。
訪問標志
緊接著常量池之后的內(nèi)容是訪問標志(上圖途中官帘,出現(xiàn)在常量池上方)。注意昧谊,這里說的是類級別的訪問標志刽虹。就像它的名字一樣,這是一個 flag呢诬,要么有要么沒有涌哲,可以取以下的值,一個類可以有多個標志尚镰。
同樣看一下我們的例子阀圾,上面使用 javap
的分析已經(jīng)知道了標志為 ACC_PUBLIC, ACC_SUPER
,我們來驗證一下狗唉。首先看源碼初烘,這僅僅是一個普通類,不是接口也沒有被聲明為抽象。由于使用的 JDK 版本為 1.8肾筐,所以 ACC_PUBLIC, ACC_SUPER
應(yīng)該為真哆料,進行計算 -> 0x0001 | 0x0020 = 0x0021
可以知道 access_flag
的值應(yīng)為 0x21
。
類信息集合
訪問標志結(jié)束之后吗铐,是當(dāng)前類信息的 3 個集合:類索引 (this_class
东亦,u2類型),父索引 (super_class
唬渗,u2類型)典阵,接口索引 (interfaces
,u2類型集合)谣妻。
看上圖萄喳,我們例子中卒稳,這三個集合的信息如下:
- 類索引: 0x0006 -> class Example
- 父索引: 0x0007 -> class java/lang/Object
- 接口索引: 0x0000
很顯然蹋半,符合我們的源碼。
字段表集合
字段表充坑,顧名思義了减江,用來描述類字段 (field) 的表,包括類變量和實例變量捻爷。在 Java 中辈灼,一個字段可以有什么來描述呢? 訪問修飾符也榄,類變量(static)還是實例變量巡莹,是否 final, volatile, transient
修飾,字段類型甜紫,字段名稱降宅。前面的 “修飾” 幾項,要么被某個關(guān)鍵字修飾囚霸,要么不被修飾腰根,可以用 flag 來表示,字段類型和名稱是不確定的不能使用標志拓型,需要引用常量池的數(shù)據(jù)項來表示额嘿。
字段表的格式如下:
在這里,訪問表示的處理和類的訪問標志的處理方法基本相同劣挫,只是描述字段的訪問標志的項目與描述類的有所不同册养,后文會給出具體的描述表。name_index, descriptor_index
的作用是指向常量池的具體條目压固。屬性表會在后文敘述球拦,它主要存儲一些額外信息,比如如果一個String
類型的字段被 static
修飾,可能字段表里會有一個條目指向常量池的常量來表示這個字段的值刘莹。
如果你仔細的在 javap
命令給出的分析結(jié)果中查看阎毅,會發(fā)現(xiàn)一些奇奇怪怪的符號,如下所示点弯。這些 I
, V
究竟表示的是什么意思呢扇调?這就是我們上問提及的 descriptor
的一部分,原來對于類型抢肛,Class
文件中用一個字符來表示標識狼钮。I
表示整形, V
表示 void
捡絮。
上面沒有提到的還有數(shù)組字段熬芜,數(shù)組字段在類型的基礎(chǔ)上會添加一個前綴 [
,這個前綴的個數(shù)取決于數(shù)組的維度福稳,比如二維數(shù)組那就會是 [[
涎拉。
方法表集合
方法表集合,用來描述類中定義的方法的圆。和字段差不多鼓拧,只是訪問表示略有不同蟀拷,并且方法表會在其屬性表中多些其他的子項楞泼,比如顷编,方法返回值類型坞琴,參數(shù)列表骑歹,方法內(nèi)部的局部變量等贞瞒。具體見下圖:
這里如果寫過 compiler 的同學(xué)就會覺得很奇怪了叼旋,方法里的代碼到哪里去了刃麸? 所有方法里的代碼會被編譯成指令放在屬性表的 code
屬性里面阎抒。相關(guān)的內(nèi)容酪我,說到字節(jié)碼執(zhí)行引擎的時候會詳細的敘述。
我們的例子中挠蛉,使用 javap
命令后祭示,能夠得到一個好看的方法表集合。
屬性表集合
屬性表谴古,這個詞在前面已經(jīng)出現(xiàn)過挺多次的了质涛。它可以出現(xiàn)在 Class
文件,字段表掰担,方法表中汇陆,用來描述某些場景的專屬信息。具體的表結(jié)構(gòu)如下圖所示带饱。屬性表與 Class
文件中的其他數(shù)據(jù)項不太相同毡代,它沒有過于嚴格的順序阅羹、長度和內(nèi)容要求。任何人實現(xiàn)的編譯器都可以向其中寫入自己定義的信息教寂, Java 虛擬機會忽略自己不認識的屬性捏鱼。當(dāng)然,虛擬機預(yù)先定義了一些屬性 (準確地說是 23 項)酪耕。由于這部分比較復(fù)雜导梆,無法逐一詳細解釋,具體可以參考 JVM Specification (這里給出的鏈接是 Java SE 8 Edition)
本節(jié)主要探討該表中的 code
屬性迂烁,該屬性位于上圖的 info
中看尼,是其中的一個子項(entry)。這個表存放的是某個方法的對應(yīng)字節(jié)指令嗎盟步。其結(jié)構(gòu)如下:
回憶一下我們是怎么把一個 .java
文件變成一個 .class
文件的藏斩。沒錯,那就是 javac
命令却盘!我們知道狰域,一個程序基本上可以分成兩大部分,數(shù)據(jù)和操作數(shù)據(jù)的語句谷炸。當(dāng)我們下這個命令的時候北专,所有的執(zhí)行語句都被變成了字節(jié)碼指令,被存放在了相應(yīng)方法表的屬性表中旬陡。除了 code
屬性里的內(nèi)容,其他的就都是程序的數(shù)據(jù)了语婴。
讓我們看看 code
表里究竟是個什么樣子描孟。
-
attribute_name_index
,指向一個字面量 “Code”砰左; -
attribute_length
匿醒,指示了 code 屬性值的長度。 -
max_stack
缠导,代表了操作數(shù)棧的最大深度廉羔,虛擬機需要根據(jù)這個值分配棧幀中的操作棧深度。這里需要稍微展開一下僻造,JVM 可以基于操作數(shù)棧憋他,也可以基于寄存器∷柘鳎“Java HotSpot(TM)” 是基于操作數(shù)棧的竹挡, “Dalvik” (Andorid 上運行的 JVM) 則是基于寄存器的; -
max_locals
立膛,代表了方法內(nèi)部的局部變量表所需要的空間揪罕,單位是 Slot梯码,Slot 是虛擬機內(nèi)部為局部變量分配內(nèi)存的最小單位。注意好啰,并不是定義了多少個局部變量轩娶,這個值就是多少,Slot 是可重用的資源框往。除了double, long
占用 2 個 Slot 以外罢坝,其他的數(shù)據(jù)類型均占 1 個 Slot; -
code_length
和code
用來存放指令的個數(shù)和具體指令搅窿。由于指令的數(shù)據(jù)類型是 u1嘁酿,所以最多只能定義 256 條,目前已經(jīng)定義了的指令有大約 200 條男应。
本節(jié)不打算深入字節(jié)指令碼闹司,但是我們可以通過我們的例子,稍微的看看沐飘。在源碼中游桩,我們有如下的方法定義:
public int inc() {
return m + 1;
}
使用 javap
命令解析后,我們可以得到如下的指令:
稍微解釋一下上面的指令:
-
aload_0
-> 將第 0 個 Slot (this
) 中為 reference 類型的本地變量推入操作數(shù)棧頂 -
gefield #2
-> 取操作數(shù)棧頂?shù)?reference 類型的變量耐朴,提取它的字段借卧,字段信息由常量池中 index 為 2 的子項決定 (this.m
) -
iconst_1
-> 將一個整形變量1
推出操作數(shù)棧 -
iadd
-> 整形加入指令,操作數(shù)數(shù)量為 2筛峭,即操作數(shù)棧頂?shù)膬蓚€元素 (this.m
和1
) -
ireturn
-> 返回一個整形變量
再多嘴一句铐刘,指令集的具體信息,可以在 JVM Specification 中第六章找到影晓。屬性表還有其他很多有用的信息镰吵,比如異常表等,但是不在這里逐一敘述了挂签,有興趣可以閱讀上面的材料疤祭。
總結(jié)
本文粗略介紹了 Class
文件的結(jié)構(gòu)。往上饵婆,可以了解 javac
產(chǎn)出的結(jié)果勺馆;往下,可以幫助理解 JVM 的字節(jié)碼執(zhí)行引擎是如何工作的侨核。
Apr 28, 2018
Montreal