Java 字節碼

1.1 什麼是字節碼?

Java 在剛剛誕生之時曾經提出過一個很是著名的口號: 「一次編寫,處處運行(write once,run anywhere)」,這句話充分表達了軟件開發人員對衝破平臺界限的渴求。「與平臺無關」的理想最終實如今操做系統的運用層上: 虛擬機提供商開發了許多能夠運行在不一樣平臺上的虛擬機,這些虛擬機均可以載入和執行同一種平臺無關的字節碼,從而實現了程序的「一次編寫處處運行」。
各類不一樣平臺的虛擬機與全部平臺都統一使用的程序存儲格式—字節碼(ByteCode),所以,能夠看出字節碼對 Java 生態的重要性。之因此被稱爲字節碼,是由於字節碼是由十六進制組成的,而 JVM(Java Virtual Machine)以兩個十六進制爲一組,即以字節爲單位進行讀取。在 Java 中使用 javac 命令把源代碼編譯成字節碼文件,一個 .java 源文件從編譯成 .class 字節碼文件的示例如圖 1 所示:
圖1<center>圖 1</center>html

<!--more-->
對於從事基於 JVM 的語言的開發人員來講,好比: Java,瞭解字節碼能夠更準確、更直觀的理解 Java 語言中更深層次的東西,好比經過字節碼,能夠很直觀的看到 volatile 關鍵字如何在字節碼上生效。另外,字節碼加強技術在各類 ORM 框架、Spring AOP、熱部署等一些應用中常用,深刻理解其原理對於咱們來講大有裨益。因爲 JVM 規範的存在,只要最終生成了符合 JVM 字節碼規範的文件均可以在 JVM 上運行,所以,這個也給其它各類運行在 JVM 上的語言(如: ScalaGroovyKotlin)提供了一個機會,能夠擴展 Java 沒有實現的特性或者實現一些語法糖。
接下來就讓咱們就一塊兒看看這個字節碼文件結構究竟是什麼樣的。java

1.2 Java 字節碼結構

Java 源文件經過用 javac 命令編譯後就會獲得 .class 結尾的字節碼文件,好比一個簡單的 JavaCodeCompilerDemo 類如圖 2 所示:
圖2<center>圖 2</center>
編譯後生成的 .class 字節碼文件,打開後是一堆 十六進制 數,如圖 3 所示:
圖3<center>圖 3</center>
在上節提過,JVM 對於字節碼規範是有要求的,打開編譯後的字節碼文件看似混亂無章,其實它是符合必定的結構規範的,JVM 規範要求每個字節碼文件都要由十部分固定的順序組成的,接下來咱們將一一介紹這部分,總體的組成結構如圖 4 所示:
圖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 所示:
圖5<center>圖 5</center>
常量池計數器(constant_pool_count): 因爲常量池的數量不固定,因此須要先放置兩個字節來表示常量池容量計數值,圖 2 示例代碼的字節碼的前十個字節以下圖 6 所示,將十六進制的 17 轉爲十進制的值爲 33 (1 16^1 + 7 16^0 = 33),排除下標 0,也就是說這個類文件有 32 個常量。
圖6<center>圖 6</center>
常量池數據區: 數據區是由(constant_pool_count - 1)個 cp_info 結構組成,一個 cp_info 的結構對應一個常量。在字節碼中共有 14 種類型的 cp_info ,每種類型的結構都是固定的,如圖 7 所示:
圖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 所示:框架

圖8<center>圖 8</center>
其它類型的 cp_info 結構在本文不在細說,和 CONSTANT_Utf8_info 的結構大同小異,都是先經過 tag 來標識類型,而後後續的 n 個字節來描述長度和數據。等咱們對這些結構比較瞭解了以後,咱們能夠經過: javap -verbose JavaCodeCompilerDemo 命令查看 JVM 反編譯後的完整常量池,能夠看到反編譯結果能夠將每個 cp_info 結構的類型和值都很明確的呈現出來,如圖 9 所示:
圖9<center>圖 9</center>jvm

(4)訪問標誌(access_flag)
常量池結束以後的兩個字節,描述該 Class 是類仍是接口,以及是否被 PublicAbstractFinal 等修飾符修飾。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 所示:
圖10<center>圖 10</center>
以圖 3 中的字節碼字段表爲例,以下圖 11 所示。其中字段的訪問標誌查表 2,002 對應爲 Private,經過索引下標在圖 9 中常量池分別獲得字段名爲: numberA,描述符爲: I(在JVM 中的I表明 Java 中的 int)。綜上,就能夠惟一肯定出類 JavaCodeCompilerDemo 中聲明的變量爲: private int numberA
圖11<center>圖 11</center>

(9)方法表(method_table)
字段表結束後爲方法表,方法表也是由兩部分組成,第一部分爲兩個字節描述方法的個數,第二個部分爲每一個方法的詳細信息。方法的詳細信息包括:方法的訪問標誌、方法名、方法的描述符以及方法的屬性,如圖 12 所示:
圖12<center>圖 12</center>
方法的權限修飾符依然能夠經過圖 9 的值查詢到,方法名和方法的描述符都是常量池的索引值,能夠經過索引值在常量池中查詢獲得。而方法屬性這個部分比較複雜,咱們能夠藉助 javap -verbose 將其反編譯爲人們可讀的信息進行解讀。如圖 13 所示。咱們能夠看到屬性中包含三個部分:

  1. Code 區: 源代碼對應的 JVM 指令操做碼,咱們在字節碼加強的時候重點操做的就是這個部分。
  2. LineNumberTable: 行號表,將 Code 區的操做碼和源代碼的行號對應,Debug 時會起到做用(即: 當源代碼向下走一行,相應的須要走幾個 JVM 指令操做碼)。
  3. LocalVariableTable: 本地變量表,包含 this 和局部變量,之因此能夠在每個非 static 的方法內部均可以調用到 this,是由於 JVM 將 this 做爲每一個方法的第一個參數隱式進行傳入。

圖13<center>圖 13</center>

(10)附加屬性表(additional_attribute_table)
字節碼的最後一部分,存放了在文件中類或接口所定義的屬性的基本信息。

1.3 Java 字節碼操做集合

在圖 13 中,Code 區的編號是 0 ~ 10,就是 .java 源文件的方法源代碼編譯後讓 JVM 真正執行的操做碼。爲了幫助人們理解,反編譯後看到的是十六進制操做碼所對應的助記符,十六進制值操做碼和助記符的對應關係,以及每一個操做碼的具體做用能夠查看 Oracle 官網,在須要的時候查閱便可。好比上圖 13 的助記符爲 iconst_2,對應圖 3 中的字節碼 0x05,做用是將 int 值 2 壓入操做數棧中。以此類推,對 0 ~ 10 的助記符理解後就是整個 sum() 方法的操做數碼實現。

1.4 查看字節碼工具

若是咱們每次反編譯都要使用 javap 命令的話,確實比較繁瑣,這裏我推薦你們一個 IDEA 插件: jclasslib。使用效果如圖 14 所示: 代碼編譯後在菜單欄: View -> Show Bytecode With jclasslib,能夠很直觀地看到當前字節碼文件的類信息、常量池、方法區等信息,很是方便。
圖14<center>圖 14</center>

1.5 總結

Java 中字節碼文件是 JVM 執行引擎的數據入口,也是 Java 技術體系的基礎構成之一。瞭解字節碼文件的組成結構對後面進一步瞭解虛擬機和深刻學習 Java 有很重要的意義。本文較爲詳細的講解了字節碼文件結構的各個組成部分,以及每一個部分的定義、數據結構和使用方法。強烈建議本身動手分析一下,會理解得更加深刻。

相關文章
相關標籤/搜索