在Java剛剛誕生的時候就提出了一個很是著名的口號:「一次編寫,處處運行。(Write Once,Run Anywhere)」。爲了實現平臺無關性,各類不一樣平臺的虛擬機都統一使用一種程序儲存格式,就是字節碼(ByteCode)。它就以二進制字節流的方式被存放在Class文件中,其中包含了Java虛擬機指令集和符號表以及其餘輔助信息。java
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。安全
通常對於數據結構的分享不免比較枯燥,可是瞭解Class文件結構是瞭解Java虛擬機的重要基礎之一。若是想比較深刻地瞭解Java虛擬機,那麼Class文件結構是不能不接觸的。我會力求在保證邏輯準確的基礎上,儘可能通俗易懂地分享,並結合實際案例。微信
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。數據結構
Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序準確地排列在Class文件中,中間沒有任何分隔符。當遇到8位字節以上的數據時,就按照高位在前的方式(最高位字節在地址最低位、最低位字節在地址最高位的順序儲存)分割成多個8位字節儲存。工具
Class文件格式採用一種相似於C語言結構體的僞結構來儲存數據的,這種僞結構有兩種數據類型:無符號數和表。this
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。編碼
無符號數用u一、u二、u四、u8分別表明1個字節、2個字節、4個字節和8個字節的無符號數,能夠用來描述數字、索引引用、數量值或者UTF-8編碼構成的字符串值。spa
表是由多個無符號數或其餘表做爲數據項構成的複合數據類型,全部的表都習慣地以「_info」結尾。表的數據結構和樹很相似,無符號數至關於它的葉子節點,其餘的表至關於它的子節點。整個Class文件就本質上也是一個表,具體結構以下:rest
類型 | 名稱 | 數量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔數 |
u2 | minor_version | 1 | 次版本號 |
u2 | major_version | 1 | 主版本號 |
u2 | constant_pool_count | 1 | 常量池容量計數值 |
cp_info | constant_pool | constant_pool_count - 1 | 常量池 |
u2 | access_flags | 1 | 訪問標誌 |
u2 | this_class | 1 | 類索引 |
u2 | super_class | 1 | 父類索引 |
u2 | interfaces_count | 1 | 接口索引計數值 |
u2 | interfaces | interface_count | 接口索引 |
u2 | fields_count | 1 | 字段計數值 |
field_info | fields | fields_count | 字段 |
u2 | methods_count | 1 | 方法計數值 |
method_info | fields | methods_count | 方法 |
u2 | attributes_count | 1 | 屬性計數值 |
attribute_info | attributes | attributes_count | 屬性 |
能夠發現,不管是無符號數仍是表,當須要描述同一種類型又數量不定的多條數據時,就會用一個前置的計數器加幾個連續的數據項的方式,這個時候咱們就把這種一系列連續的某種類型的數據叫作這個類型的集合。code
在Class文件中,不管是順序仍是數量,甚至是數據存儲的字節序,都必須嚴格按照上面表格進行設定,哪一個字節表明什麼含義,長度是多少,前後順序怎麼樣,都不容許改變。接下來看一下各個數據項的具體含義。
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
魔數(Magic Number)是每一個Class文件的前4個字節,它用來肯定當前文件是不是一個被Java虛擬機所接受的Class文件。不少文件存儲標準中都使用了魔數進行身份識別,好比gif、jpeg等圖片文件中都有魔數。使用魔數而不使用擴展名是出於安全考慮,由於擴展名更容易被修改。文件格式制定者能夠自主選擇魔數,只要這個魔數沒有被普遍使用又不和其餘文件混淆就能夠。
Class文件的魔數是:0xCAFEBABE(咖啡寶貝?),這個魔數在Java還被稱爲「Oak」語言的時候(大概是1991年)就肯定下來了,據Java開發小組最初的關鍵成員Patrick Naughton說:「咱們一直在尋找一些好玩的、容易記憶的東西,選擇0xCAFEBABE是由於它象徵着著名咖啡品牌Peet's Coffee中深受歡迎的Baristas咖啡」,他們是真的很喜歡喝咖啡啊,可能也預示着往後「Java」這個名字的出現。
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
爲了更快的理解,我準備了一個實際案例,一段很是簡單的Java代碼:
public class OneMoreStudy { private int number; private int plusOne() { return number + 1; } }
使用JDK 1.7把這段代碼編譯成Class文件,用HexEd打開,就能夠到魔數了,以下圖:
在接下來的分享中,也會常用這個Class文件。
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
緊跟着魔數的第5和第6個字節是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version)。Java的主版本號是從45開始的,從JDK 1.1之後每一個JDK大版本發佈主版本號都加1,高版本的JDK向下兼容低版本的Class文件,但不能運行更高版本的Class文件,即便Class文件的格式沒有發生任何變化,Java虛擬機也會拒絕運行超過其版本號的Class文件。
再來看一下以前的Class文件例子:
表示次版本號的第5和第6個字節值爲0x0000,表示主版本號的第7和第8個字節值爲0x0033,也就是十進制的51,說明這個Class文件能夠被JDK 1.7及其以上版本的Java虛擬機運行。
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
緊跟着主版本號的就是常量池,它能夠理解爲Class文件的資源倉庫,也是Class文件結構中與其餘數據項關聯最多的數據類型。由於在常量池中的常量數量是不固定的,因此首先有一個u2類型的數據,表示常量池容量大小(constant_pool_count)。
常量池的容量計數不是從0開始的,而是從1開始的,這是由於0有它的特殊用用途,那就是爲了表達在特殊狀況下須要表達「不引用任何一個常量池項目」的含義。在Class文件結構中只有常量池的容量計數是從1開始的,對於其餘集合,包括接口索引集合、字段集合、方法集合等的容量計數都是從0開始的。
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
再來看一下以前的Class文件例子:
常量池容器計數值爲0x0013,也就是十進制的19,它表示常量池中有18個常量,索引值範圍從1到18。
常量池中主要存儲兩種常量:字面量(Literal)和符號引用(Symbolic References)。字面量比較接近Java語言層面的常量,好比文本字符串、聲明爲final的常量值。符號引用則是編譯原理層次的概念,它包括如下三種:
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
常量池中每個常量都是一個表,共有14種不一樣的常量類型(JDK1.7及以前版本),每一種類型的表在第一位都有一個u1類型的標誌位,具體以下表:
類型 | 標誌位 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8編碼的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 標識方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
有個一個專門分析Class文件字節碼的工具javap,咱們用它直接看一下以前的Class文件例子裏的18個常量(常量池之外的信息已省略):
E:\>javap -verbose OneMoreStudy Compiled from "OneMoreStudy.java" minor version: 0 major version: 51 Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // OneMoreStudy.number:I #3 = Class #17 // OneMoreStudy #4 = Class #18 // java/lang/Object #5 = Utf8 number #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 plusOne #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 OneMoreStudy.java #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // number:I #17 = Utf8 OneMoreStudy #18 = Utf8 java/lang/Object
其中,有一些常量好像在代碼裏沒有出現過,如「I」、「
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
緊跟着常量池的2個字節表示訪問標誌(access_flags),它用於識別一些類或接口層次的訪問信息,具體見下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否爲public類型 |
ACC_FINAL | 0x0010 | 是否被聲明爲final |
ACC_SUPER | 0x0020 | 是否容許使用invokespecial字節碼指令 |
ACC_INTERFACE | 0x0200 | 是不是接口 |
ACC_ABSTRACT | 0x0400 | 是否爲abstract類型 |
ACC_SYNTHETIC | 0x1000 | 標誌這個類並不是由用戶代碼產生的 |
ACC_ANNOTATION | 0x2000 | 是不是註解 |
ACC_ENUM | 0x4000 | 是不是枚舉 |
其中,ACC_SUPER在JDK 1.0.2以後編譯出來的Class文件必須爲true;ACC_ABSTRACT對於接口或抽象類來講爲true,其餘類爲false。
以前的例子OneMoreStudy是一個普通的類,不是接口、註解或枚舉,只被public修飾,沒有被聲明爲final或abstract,並且是JDK 1.7編譯的,因此只有ACC_PUBLIC和ACC_SUPER爲true,因此它的訪問標誌應該是0x0001 | 0x0020 = 0x0021,以下圖:
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。
因爲篇幅限制,此次的分享先暫時到這裏,但願你們更好地消化吸取。欲知後事如何,請聽下回分解!敬請期待!
歡迎關注微信公衆號:萬貓學社,每週一分享Java技術乾貨。