Java 在剛剛誕生之時曾經提出過一個很是著名的口號: 「一次編寫,處處運行(write once,run anywhere)」,這句話充分表達了軟件開發人員對衝破平臺界限的渴求。「與平臺無關」的理想最終實如今操做系統的運用層上: 虛擬機提供商開發了許多能夠運行在不一樣平臺上的虛擬機,這些虛擬機均可以載入和執行同一種平臺無關的字節碼,從而實現了程序的「一次編寫處處運行」。
各類不一樣平臺的虛擬機與全部平臺都統一使用的程序存儲格式—字節碼(ByteCode),所以,能夠看出字節碼對 Java 生態的重要性。之因此被稱爲字節碼,是由於字節碼是由十六進制組成的,而 JVM(Java Virtual Machine)以兩個十六進制爲一組,即以字節爲單位進行讀取。在 Java 中使用 javac 命令把源代碼編譯成字節碼文件,一個 .java 源文件從編譯成 .class 字節碼文件的示例如圖 1 所示:<center>圖 1</center>html
<!--more-->
對於從事基於 JVM 的語言的開發人員來講,好比: Java,瞭解字節碼能夠更準確、更直觀的理解 Java 語言中更深層次的東西,好比經過字節碼,能夠很直觀的看到 volatile 關鍵字如何在字節碼上生效。另外,字節碼加強技術在各類 ORM 框架、Spring AOP、熱部署等一些應用中常用,深刻理解其原理對於咱們來講大有裨益。因爲 JVM 規範的存在,只要最終生成了符合 JVM 字節碼規範的文件均可以在 JVM 上運行,所以,這個也給其它各類運行在 JVM 上的語言(如: Scala、Groovy、Kotlin)提供了一個機會,能夠擴展 Java 沒有實現的特性或者實現一些語法糖。
接下來就讓咱們就一塊兒看看這個字節碼文件結構究竟是什麼樣的。java
Java 源文件經過用 javac 命令編譯後就會獲得 .class 結尾的字節碼文件,好比一個簡單的 JavaCodeCompilerDemo 類如圖 2 所示:<center>圖 2</center>
編譯後生成的 .class 字節碼文件,打開後是一堆 十六進制 數,如圖 3 所示:<center>圖 3</center>
在上節提過,JVM 對於字節碼規範是有要求的,打開編譯後的字節碼文件看似混亂無章,其實它是符合必定的結構規範的,JVM 規範要求每個字節碼文件都要由十部分固定的順序組成的,接下來咱們將一一介紹這部分,總體的組成結構如圖 4 所示:<center>圖 4</center>windows
(1)魔數(Magic Number)
每一個字節碼文件的頭 4 個字節稱爲 魔數(Magic Number),它的惟一做用是肯定這個文件是否爲一個能被虛擬機接受的 Class 文件。不少文件存儲標準中都使用魔數來進行身份識別,譬如圖片格式,如 gif 或者 jpg 等在文件頭中都存有魔數。使用魔數而不是擴展名來進行識別主要是基於安全方面的考慮,由於文件擴展名能夠隨意改動。魔數的固定值爲: 0xCAFEBABE,魔數放在文件頭,JVM 能夠根據文件的開頭來判斷這個文件是否多是一個字節碼文件,若是是,纔會進行以後的操做。安全
有趣的是,魔數的固定值是 Java 之父 James Gosling 制定的,爲 CafeBabe(咖啡寶貝),而 Java 的圖標爲一杯咖啡。
(2)版本號(Version)
版本號爲魔數以後的 4 個字節,前兩個字節表示次版本號(Minor Version),後兩個字節表示主版本號(Major Version),上圖 3 中版本號爲: 「00 00 00 34」,次版本號轉化爲十進制爲 0,主版本號轉化爲十進制 52(3 16^1 + 4 16^0 = 52),在 Oracle 官網中查詢序號 52 對應的 JDK 版本爲 1.8,因此編譯該源代碼文件的 Java 版本爲 1.8.0。數據結構
(3)常量池(Constant Pool)
緊接着主版本號以後的字節是常量池入口。常量池中存儲兩種類型常量: 字面量和符號運用。字面量爲代碼中聲明爲 final 的常量值,符號引用如類和接口的全侷限定名、字段的名稱和描述符、方法的名稱和描述符。常量池總體上分爲兩部分: 常量池計數器和常量池數據區,如圖 5 所示:<center>圖 5</center>
常量池計數器(constant_pool_count): 因爲常量池的數量不固定,因此須要先放置兩個字節來表示常量池容量計數值,圖 2 示例代碼的字節碼的前十個字節以下圖 6 所示,將十六進制的 17 轉爲十進制的值爲 33 (1 16^1 + 7 16^0 = 33),排除下標 0,也就是說這個類文件有 32 個常量。<center>圖 6</center>
常量池數據區: 數據區是由(constant_pool_count - 1)個 cp_info 結構組成,一個 cp_info 的結構對應一個常量。在字節碼中共有 14 種類型的 cp_info ,每種類型的結構都是固定的,如圖 7 所示:<center>圖 7</center>
以 CONSTANT_Utf8_info 爲例,它的結構如表 1 所示:oracle
名稱 | 長度 | 值 |
---|---|---|
tag | 1 字節 | 01 對應圖 7 中 CONSTANT_Utf8_info 的標誌欄中的值 |
length | 2 字節 | 該 utf8 字符串的長度 |
bytes | length 字節 | length 個字節的具體數據 |
<center>表 1</center>
首先第一個字節 tag,它的取值對應圖 7 中的 Tag,因爲它的類型是 CONSTANT_Utf8_info,因此值爲 01(十六進制)。接下來兩個字節標識該字符串的長度 length,而後 length 個字節爲這個字符串具體的值。從圖 3 的字節碼中摘取一個 cp_info 結構,將它翻譯過來後,其含義爲: 該常量爲 utf8 字符串,長度爲 7 字節,數據爲: numberA,如圖 8 所示:框架
<center>圖 8</center>
其它類型的 cp_info 結構在本文不在細說,和 CONSTANT_Utf8_info 的結構大同小異,都是先經過 tag 來標識類型,而後後續的 n 個字節來描述長度和數據。等咱們對這些結構比較瞭解了以後,咱們能夠經過: javap -verbose JavaCodeCompilerDemo 命令查看 JVM 反編譯後的完整常量池,能夠看到反編譯結果能夠將每個 cp_info 結構的類型和值都很明確的呈現出來,如圖 9 所示:<center>圖 9</center>jvm
(4)訪問標誌(access_flag)
常量池結束以後的兩個字節,描述該 Class 是類仍是接口,以及是否被 Public、Abstract、Final 等修飾符修飾。JVM 規範規定了以下表 2 所示的 9 種訪問標誌。須要注意的是,JVM 並無窮舉全部的訪問標誌,而是使用 按位或 操做來進行描述的,好比某個類的修飾符爲 public final,則對應的訪問修飾符的值爲 ACC_PUBLIC | ACC_FINAL,即 0x0001 | 0x0010 = 0x0011。工具
標誌名稱 | 標誌值 | 含義 |
---|---|---|
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_SYNCHETIC | 0x1000 | 字段是否爲編譯器自動產生 |
ACC_ENUM | 0x4000 | 字段是否爲 enum |
<center>表 2</center>學習
(5)當前類名(this_class)
訪問標誌後的兩個字節,描述的是當前類的全限定名。這兩個字節保存的值爲常量池中的索引值,根據索引值就能在常量池中找到這個類的全限定名。
(6)父類名稱(super_class)
當前類名的後兩個字節,描述父類的全限定名。這兩個字節保存的值也是在常量池中的索引值,根據索引值就能在常量池中找到這個類的父類的全限定名。
(7)接口信息(interfaces)
父類名稱後的兩個字節,描述這個類的接口計數器,即: 當前類或父類實現的接口數量。緊接着的 n 個字節是全部的接口名稱的字符串常量在常量池的索引值。
(8)字段表(field_table)
字段表用於描述類和接口中聲明的變量,包含類級別的變量以及實例變量,可是不包含方法內部聲明的 局部變量。字段表也分爲兩部分,第一部分是兩個字節,描述字段個數,第二部分是每一個字段的詳細信息 field_info。字段表結構如圖 10 所示:<center>圖 10</center>
以圖 3 中的字節碼字段表爲例,以下圖 11 所示。其中字段的訪問標誌查表 2,002 對應爲 Private,經過索引下標在圖 9 中常量池分別獲得字段名爲: numberA,描述符爲: I(在JVM 中的I表明 Java 中的 int)。綜上,就能夠惟一肯定出類 JavaCodeCompilerDemo 中聲明的變量爲: private int numberA 。<center>圖 11</center>
(9)方法表(method_table)
字段表結束後爲方法表,方法表也是由兩部分組成,第一部分爲兩個字節描述方法的個數,第二個部分爲每一個方法的詳細信息。方法的詳細信息包括:方法的訪問標誌、方法名、方法的描述符以及方法的屬性,如圖 12 所示:<center>圖 12</center>
方法的權限修飾符依然能夠經過圖 9 的值查詢到,方法名和方法的描述符都是常量池的索引值,能夠經過索引值在常量池中查詢獲得。而方法屬性這個部分比較複雜,咱們能夠藉助 javap -verbose 將其反編譯爲人們可讀的信息進行解讀。如圖 13 所示。咱們能夠看到屬性中包含三個部分:
<center>圖 13</center>
(10)附加屬性表(additional_attribute_table)
字節碼的最後一部分,存放了在文件中類或接口所定義的屬性的基本信息。
在圖 13 中,Code 區的編號是 0 ~ 10,就是 .java 源文件的方法源代碼編譯後讓 JVM 真正執行的操做碼。爲了幫助人們理解,反編譯後看到的是十六進制操做碼所對應的助記符,十六進制值操做碼和助記符的對應關係,以及每一個操做碼的具體做用能夠查看 Oracle 官網,在須要的時候查閱便可。好比上圖 13 的助記符爲 iconst_2,對應圖 3 中的字節碼 0x05,做用是將 int 值 2 壓入操做數棧中。以此類推,對 0 ~ 10 的助記符理解後就是整個 sum() 方法的操做數碼實現。
若是咱們每次反編譯都要使用 javap 命令的話,確實比較繁瑣,這裏我推薦你們一個 IDEA 插件: jclasslib。使用效果如圖 14 所示: 代碼編譯後在菜單欄: View -> Show Bytecode With jclasslib,能夠很直觀地看到當前字節碼文件的類信息、常量池、方法區等信息,很是方便。<center>圖 14</center>
Java 中字節碼文件是 JVM 執行引擎的數據入口,也是 Java 技術體系的基礎構成之一。瞭解字節碼文件的組成結構對後面進一步瞭解虛擬機和深刻學習 Java 有很重要的意義。本文較爲詳細的講解了字節碼文件結構的各個組成部分,以及每一個部分的定義、數據結構和使用方法。強烈建議本身動手分析一下,會理解得更加深刻。