走進 Class 文件

走進 Class 文件

本文是作者原創(chuàng)文章,如需轉(zhuǎn)載奴璃,請先聯(lián)系

首先發(fā)布于我的個人網(wǎng)站

Reference

  1. <<深入理解 Java 虛擬機>> 周志明著
  2. << 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");
    }
}

打開命令行闲坎,編譯并且運行一下這個例子的源碼:

image

這么看疫粥,并沒有什么特別的,僅僅是一個最最最簡單的 “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)容:

image

一個小小的 “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)
image

下圖顯示了一個完整的 Class 文件的結(jié)構(gòu)俭尖,后文將逐一介紹這幾個部分。

image

魔數(shù)與版本號

Class 文件中的所有數(shù)據(jù)都是按照嚴格規(guī)定的順序進行排列的,“魔數(shù)” 就是文件中出現(xiàn)的第一個數(shù)據(jù)項稽犁。所有由 javac 命令編譯生成的 .calss 文件前四個字節(jié)都是一樣的焰望,稱為“魔數(shù) (magic number)”,內(nèi)容為 CAFEBABE缭付。

Class 文件的版本號分為兩個部分:

  1. Minor Version柿估,第 5 和 第 6 字節(jié)循未;
  2. Major Version陷猫,第 7 和 第 8 字節(jié);
image

上圖 major version 中的 34 是十六進制的妖,它代表十進制中的 52绣檬,下表為 Java VersionMajor Version 的對照表,可以看到 52 對應(yīng)的是 Java 8嫂粟。

image

下面的是我機器上的 Java 版本娇未,可以看到是 Java 1.8.0_51 和上面的結(jié)果符合。

image

常量池

常量池 (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)
image

下面甩苛,我們嘗試著分析一下我們一開始的例子里面的常量池數(shù)據(jù)蹂楣。從下圖中我們可以看到,計數(shù)器的數(shù)值轉(zhuǎn)化成十進制后是 35讯蒲。這表明一共有 34 項常量 1 ~ 34痊土,零保留下來了,表示 “不引用常量池的任何一個項目”墨林。

image

緊接著計數(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

可以看到輸入如下 (和我們分析的吻合):

image

后文不再逐一分析常量池的內(nèi)容箱舞,有需要可以自行驗證多幾項遍坟。總而言之晴股,常量池里的內(nèi)容如果是字面量就可以被別的表引用愿伴,如果是符號引用則會繼續(xù)引用別的表。多一句嘴队魏,這寫符號引用會在某個階段被替換成內(nèi)存中的地址公般,在傳統(tǒng)的編譯型語言中 (比如 C/C++),這一步驟被稱為 “鏈接 (linking)”胡桨。

訪問標志

緊接著常量池之后的內(nèi)容是訪問標志(上圖途中官帘,出現(xiàn)在常量池上方)。注意昧谊,這里說的是類級別的訪問標志刽虹。就像它的名字一樣,這是一個 flag呢诬,要么有要么沒有涌哲,可以取以下的值,一個類可以有多個標志尚镰。

image

同樣看一下我們的例子阀圾,上面使用 javap 的分析已經(jīng)知道了標志為 ACC_PUBLIC, ACC_SUPER,我們來驗證一下狗唉。首先看源碼初烘,這僅僅是一個普通類,不是接口也沒有被聲明為抽象。由于使用的 JDK 版本為 1.8肾筐,所以 ACC_PUBLIC, ACC_SUPER應(yīng)該為真哆料,進行計算 -> 0x0001 | 0x0020 = 0x0021 可以知道 access_flag 的值應(yīng)為 0x21

image

類信息集合

訪問標志結(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ù)項來表示额嘿。

字段表的格式如下:

image

在這里,訪問表示的處理和類的訪問標志的處理方法基本相同劣挫,只是描述字段的訪問標志的項目與描述類的有所不同册养,后文會給出具體的描述表。name_index, descriptor_index 的作用是指向常量池的具體條目压固。屬性表會在后文敘述球拦,它主要存儲一些額外信息,比如如果一個String類型的字段被 static修飾,可能字段表里會有一個條目指向常量池的常量來表示這個字段的值刘莹。

image

如果你仔細的在 javap 命令給出的分析結(jié)果中查看阎毅,會發(fā)現(xiàn)一些奇奇怪怪的符號,如下所示点弯。這些 I, V究竟表示的是什么意思呢扇调?這就是我們上問提及的 descriptor的一部分,原來對于類型抢肛,Class 文件中用一個字符來表示標識狼钮。I 表示整形, V 表示 void捡絮。

image
image

上面沒有提到的還有數(shù)組字段熬芜,數(shù)組字段在類型的基礎(chǔ)上會添加一個前綴 [,這個前綴的個數(shù)取決于數(shù)組的維度福稳,比如二維數(shù)組那就會是 [[涎拉。

方法表集合

方法表集合,用來描述類中定義的方法的圆。和字段差不多鼓拧,只是訪問表示略有不同蟀拷,并且方法表會在其屬性表中多些其他的子項楞泼,比如顷编,方法返回值類型坞琴,參數(shù)列表骑歹,方法內(nèi)部的局部變量等贞瞒。具體見下圖:

image

這里如果寫過 compiler 的同學(xué)就會覺得很奇怪了叼旋,方法里的代碼到哪里去了刃麸? 所有方法里的代碼會被編譯成指令放在屬性表的 code 屬性里面阎抒。相關(guān)的內(nèi)容酪我,說到字節(jié)碼執(zhí)行引擎的時候會詳細的敘述。

我們的例子中挠蛉,使用 javap 命令后祭示,能夠得到一個好看的方法表集合。

image

屬性表集合

屬性表谴古,這個詞在前面已經(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)

image

本節(jié)主要探討該表中的 code屬性迂烁,該屬性位于上圖的 info 中看尼,是其中的一個子項(entry)。這個表存放的是某個方法的對應(yīng)字節(jié)指令嗎盟步。其結(jié)構(gòu)如下:

image

回憶一下我們是怎么把一個 .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_lengthcode用來存放指令的個數(shù)和具體指令搅窿。由于指令的數(shù)據(jù)類型是 u1嘁酿,所以最多只能定義 256 條,目前已經(jīng)定義了的指令有大約 200 條男应。

本節(jié)不打算深入字節(jié)指令碼闹司,但是我們可以通過我們的例子,稍微的看看沐飘。在源碼中游桩,我們有如下的方法定義:

public int inc() {
        return m + 1;
    }

使用 javap 命令解析后,我們可以得到如下的指令:

image

稍微解釋一下上面的指令:

  • 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.m1
  • ireturn -> 返回一個整形變量

再多嘴一句铐刘,指令集的具體信息,可以在 JVM Specification 中第六章找到影晓。屬性表還有其他很多有用的信息镰吵,比如異常表等,但是不在這里逐一敘述了挂签,有興趣可以閱讀上面的材料疤祭。

總結(jié)

本文粗略介紹了 Class 文件的結(jié)構(gòu)。往上饵婆,可以了解 javac 產(chǎn)出的結(jié)果勺馆;往下,可以幫助理解 JVM 的字節(jié)碼執(zhí)行引擎是如何工作的侨核。


Apr 28, 2018
Montreal

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末草穆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子芹关,更是在濱河造成了極大的恐慌续挟,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侥衬,死亡現(xiàn)場離奇詭異诗祸,居然都是意外死亡跑芳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門直颅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來博个,“玉大人,你說我怎么就攤上這事功偿∨栌叮” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵械荷,是天一觀的道長共耍。 經(jīng)常有香客問我,道長吨瞎,這世上最難降的妖魔是什么痹兜? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮颤诀,結(jié)果婚禮上字旭,老公的妹妹穿的比我還像新娘。我一直安慰自己崖叫,他們只是感情好遗淳,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著心傀,像睡著了一般屈暗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上剧包,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天恐锦,我揣著相機與錄音,去河邊找鬼疆液。 笑死,一個胖子當(dāng)著我的面吹牛陕贮,可吹牛的內(nèi)容都是我干的堕油。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼肮之,長吁一口氣:“原來是場噩夢啊……” “哼掉缺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起戈擒,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤眶明,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后筐高,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搜囱,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡丑瞧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜀肘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绊汹。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扮宠,靈堂內(nèi)的尸體忽然破棺而出西乖,到底是詐尸還是另有隱情,我是刑警寧澤坛增,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布获雕,位于F島的核電站,受9級特大地震影響收捣,放射性物質(zhì)發(fā)生泄漏届案。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一坏晦、第九天 我趴在偏房一處隱蔽的房頂上張望萝玷。 院中可真熱鬧,春花似錦昆婿、人聲如沸球碉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽睁冬。三九已至,卻和暖如春看疙,著一層夾襖步出監(jiān)牢的瞬間豆拨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工能庆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留施禾,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓搁胆,卻偏偏與公主長得像弥搞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子渠旁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

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