《深入理解java虛擬機》筆記4——類文件結構

代碼編譯的結果從本地機器碼轉變爲字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。

由於最近十年內虛擬機以及大量建立在虛擬機之上的程序語言如雨後春筍般出現並蓬勃發展,將我們編寫的程序編譯成二進制本地機器碼(Native Code)已不再是唯一的選擇,越來越多的程序語言選擇了操作系統和機器指令集無關的、平臺中立的格式作爲程序編譯後的存儲格式。

無關性的基石

  • Java剛誕生的宣傳口號:一次編寫,到處運行(Write Once, Run Anywhere)。其最終實現在操作系統的應用層:Sun公司以及其他虛擬機提供商發佈了許多可以運行在各種不同平臺的虛擬機,這些虛擬機都可以載入和執行同一種平臺無關的字節碼。
  • 字節碼(ByteCode)是構成平臺無關的基石;
  • 另外虛擬機的語言無關性也越來越被開發者所重視,JVM設計者在最初就考慮過實現讓其他語言運行在Java虛擬機之上的可能性,如今已發展出一大批在JVM上運行的語言,比如Clojure、Groovy、JRuby、Jython、Scala;
  • 實現語言無關性的基礎仍是虛擬機和字節碼存儲格式,Java虛擬機不和包括Java在內的任何語言綁定,它只與Class文件這種特定的二進制文件格式所關聯,這使得任何語言的都可以使用特定的編譯器將其源碼編譯成Class文件,從而在虛擬機上運行。

Java虛擬機提供的語言無關性

Class類文件的結構

  • Class文件是一組以8個字節爲基礎單位的二進制流(可能是磁盤文件,也可能是類加載器直接生成的),各個數據項目嚴格按照順序緊湊地排列,中間沒有任何分隔符;
  • Class文件格式採用一種類似於C語言結構體的僞結構來存儲數據,其中只有兩種數據類型:無符號數和表;
  • 無符號數屬於基本的數據類型,以u1、u2、u4和u8來分別代表1個字節、2個字節、4個字節和8個字節的無符號數,可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值;
  • 表是由多個無符號數獲取其他表作爲數據項構成的複合數據類型,習慣以“_info”結尾;
  • 無論是無符號數還是表,當需要描述同一個類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時稱這一系列連續的某一類型的數據未某一類型的集合。

下面我以自己本機寫的一個簡單的Java文件來學習其中各個部分的含義:


使用javac編譯成TestClass.class文件,使用16進制打開:

使用javap命令輸出Class文件信息:

魔數和版本(magic、version)

  • Class文件的頭4個字節,唯一作用是確定文件是否爲一個可被虛擬機接受的Class文件,固定爲“0xCAFEBABE”。
  • 第5和第6個字節是次版本號,第7和第8個字節是主版本號(0x0034爲52,對應JDK版本1.8);能向下兼容之前的版本,無法運行後續的版本;

常量池(constant_pool)

  • 常量池可以理解爲Class文件之中的資源倉庫,是Class文件結構中與其他項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項之一;
  • 由於常量池中的常量數量不固定,因此需要在常量池前放置一項u2類型的數據來表示容量,該值是從1開始的,上圖的0x0013爲十進制的19,代表常量池中有18項常量,索引值範圍爲1~18;
  • 常量池主要存放兩大類常量:字面量(Literal,比較接近Java的常量概念,比如文本字符串和final常量等)和符號引用(Symbolic References,主要包括類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符);
  • Java代碼在javac編譯時不會有“連接”這一步驟,而是在虛擬機加載Class文件的時候進行動態連接;所以在Class文件不會保存各個方法、字段和最終內存佈局信息;當虛擬機運行時需要從常量池獲取對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址中;
  • JDK 1.7中常量池共有14種不同的表結構數據,這些表結構開始的第一位是一個u1類型的標誌位,代表當前常量的類型,具體如下圖所示:
  • 之所以說常量池是最繁瑣的數據就是因爲這14種常量類型都有自己的結結構。可以結合下圖中各個表結構的說明和之前使用javap解析的文件內容一起看。
  • 第1項:0x0A(15標誌爲方法句柄),0x0004(指向第4項的類描述符),0x000F(指向第15項的名稱及類型描述符);
  • 第2項:0x09(9標誌爲字段符號引用),0x0003(指向第3項類描述符),0x0010(指向第16項的名稱及類型描述符);
  • 第3項:0x07(7標誌爲類符號引用),0x0011(指向第17項全限定名常量項);
  • 第4項:0x07(7標誌爲類符號引用),0x0012(指向第18項全限定名常量項);
  • 第5項:0x01(1標誌爲UTF-字符串常量),0x0001(字符串佔用1個字節),6D(字符“m”);
  • 第6項:0x01(1標誌爲UTF-字符串常量),0x0001(字符串佔用1個字節),49(字符“I”);
  • 第7項:0x01(1標誌爲UTF-字符串常量),0x0006(字符串佔用6個字節),3C 69 6E 69 74 3E(字符“<init>”);
  • 第8項:0x01(1標誌爲UTF-字符串常量),0x0003(字符串佔用3個字節),28 29 56(字符“()V”);
  • 第9項:0x01(1標誌爲UTF-字符串常量),0x0004(字符串佔用4個字節),43 6F 64 65(字符“Code”);
  • 第10項:0x01(1標誌爲UTF-字符串常量),0x000F(字符串佔用15個字節),4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65(字符“LineNumberTable”);
  • 第11項:0x01(1標誌爲UTF-字符串常量),0x0003(字符串佔用3個字節),69 6E 63(字符“inc”);
  • 第12項:0x01(1標誌爲UTF-字符串常量),0x0003(字符串佔用3個字節),28 29 49(字符“()I”);
  • 第13項:0x01(1標誌爲UTF-字符串常量),0x000A(字符串佔用10個字節),53 6F 75 72 63 65 46 69 6C 65(字符“SourceFile”);
  • 第14項:0x01(1標誌爲UTF-字符串常量),0x000E(字符串佔用14個字節),54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61(字符“TestClass.java”);
  • 第15項:0x0C(12標誌爲名稱和類型符號引用),0x0007(指向第7項名稱常量項), 0x0008(指向第8項描述符常量項);
  • 第16項:0x0C(12標誌爲名稱和類型符號引用),0x0005(指向第5項名稱常量項), 0x0006(指向第6項描述符常量項);
  • 第17項:0x01(1標誌爲UTF-字符串常量),0x001F(字符串佔用31個字節),63 6F 6D 2F 67 69 6E 6F 62 65 66 75 6E 6E 79 2F 63 6C 61 7A 7A 2F 54 65 73 74 43 6C 61 73 73(字符“com/ginobefunny/clazz/TestClas”);
  • 第18項:0x01(1標誌爲UTF-字符串常量),0x0010(字符串佔用16個字節),6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74(字符“java/lang/Object”);

訪問標誌

  • 緊接在常量池後面的是兩個字節的訪問標誌,用於標識類或接口的訪問信息;
  • 訪問標誌一個有16個標誌位,但目前只採用了其中8位,本例子中的0x0021標識爲一個public的普通類;

類索引、父類索引與接口索引集合

  • 類索引:u2類型的數據,用於確定類的全限定名。本例子中爲0x0003,指向常量池中第3項;
  • 父類索引:u2類型的數據,用於確定父類的全限定名。本例子中爲0x0004,指向常量池中第4項;
  • 接口索引計算器:u2類型的數據,用於表示索引集合的容量。本例子中爲0x0000,說明沒有實現接口;
  • 接口索引集合:一組u2類型的數據的集合,用於確定實現的接口(對於接口來說就是extend的接口)。本例子不存在。

字段表集合

  • 用於描述接口或者類中聲明的變量,包括類級變量和實例級變量,但不包括方法內部聲明的局部變量;它不會列出從父類和超類繼承而來的字段;
  • 0x0001表示這個類只有一個字段表數據;
  • 字段修飾符放在access_flag中,是一個u2的數據類型,0x0002表示爲private的屬性;
  • 字段名稱name_index,是一個u2的數據類型,0x0005表示該屬性的名稱爲常量池的第5項;
  • 字段描述符descriptor_index,是一個u2的數據類型,0x0006表示該屬性的描述符爲常量池的第6項,其值“I”表示類型爲整形;
  • 字段屬性計算器和屬性集合:0x0000表示該例子中不存在;

方法表集合

  • 和字段表集合的方式幾乎一樣;
  • 方法裏面的代碼經過編譯器編譯成字節碼指令後,存放在方法屬性表集合中一個名爲Code的屬性裏面;
  • 0x0002表示這個類有兩個方法表數據,分別是編譯器添加的實例構造器<init>和源碼中的方式inc();
  • 第一個方法的訪問標誌是0x0001(public方法),名稱索引值爲0x0007(常量池第7項,“<init>”),描述符索引值爲0x0008(常量池第8項,“()V”),屬性表計算器爲0x0001(有一項屬性),屬性名稱索引爲0x0009(常量池第9項,“Code”);
  • 根據“6.3.7.1 Code屬性”說明,屬性值的長度爲23(0x0000001D表示29,但需要減去屬性名稱索引和屬性長度固定的6個字節長度),操作數棧深度的最大值爲1(0x0001,虛擬機運行時根據這個值來分配棧幀中操作棧深度),局部變量表所需要的存儲空間爲1個Slot(0x0001,Slot是內存分配的最小單位),字節碼長度爲5(0x00000005),分別爲2A(aload_0,將第0個Slot中爲reference類型的本地變量推送到操作數棧頂)、B7(invokespecial,以棧頂的reference類型的數據所指向的對象作爲方法接收者,調用此對象的實例構造器方法、private方法或者它父類的方法,後面接着一個u2的參數指向常量池的方法引用)、0x0001(表示常量池的第1項,即Object類的<init>方法)、B1(對應的指令爲return,返回值爲void);顯式異常表爲空(0x0000,計數器爲0);該Code屬性還內嵌1個屬性(0x0001),屬性的名稱索引爲0x000A(即“LineNumberTable”屬性,用於記錄對應的代碼行數),該內嵌屬性的長度爲6(0x00000006),對應的行數信息爲源碼的第3行(0x000100000003);
  • 第二個方法的訪問標誌是0x0001(public方法),名稱索引值爲0x000B(常量池第11項,“inc”),描述符索引值爲0x000C(常量池第12項,“()I”),屬性表計算器爲0x0001(有一項屬性),屬性名稱索引爲0x0009(常量池第9項,“Code”);
  • 根據“6.3.7.1 Code屬性”說明,屬性值的長度爲25(0x0000001F表示31,但需要減去屬性名稱索引和屬性長度固定的6個字節長度),操作數棧深度的最大值爲2(0x0002),局部變量表所需要的存儲空間爲1個Slot(0x0001),字節碼長度爲7(0x00000007),分別爲2A(aload_0)、B4(getfield,後面接着一個u2的參數指向常量池的屬性引用)、0x0002(表示常量池的第2項,即TestClass類的m屬性)、04(對應的指令爲iconst_1)、60(對應的指令爲iadd,整形求和)、AC(對應的指令爲ireturn,返回值爲整形);顯式異常表爲空(0x0000,計數器爲0);該Code屬性還內嵌1個屬性(0x0001),屬性的名稱索引爲0x000A(即“LineNumberTable”屬性,用於記錄對應的代碼行數),該內嵌屬性的長度爲6(0x00000006),對應的行數信息爲源碼的第8行(0x000100000008);

屬性表集合

  • 在Class文件、字段表、方法表都可以攜帶自己的屬性表集合;
  • 屬性表集合的限制較爲寬鬆,不再要求嚴格的順序,只要屬性名不重複即可;
  • 以下是Java虛擬機規範裏預定義的虛擬機實現應當能識別的屬性:

虛擬機規範預定義的屬性2

  • 接着我們的例子的Class文件還有最後一段:0x0001表示該Class有一個屬性,0x000D表示屬性名索引爲第13項(對應“SourceFile”),0x00000002表示該屬性長度爲2,0x000E表示該類的SourceFile名稱爲第14項(對應“TestClass.java”)。
Code屬性

Java程序方法體中的代碼經過javac編譯後,字節碼指令存放在Code屬性,其屬性表結構如下:

Exceptions屬性

方法描述時throws關鍵字後面列舉的異常,和Code屬性裏的異常表不同。其屬性表結構如下:

LineNumberTable屬性

用於描述Java源碼行號與字節碼行號之間的對應關係,它不是必須的,可以通過javac -g:none取消該信息。沒有該信息的影響是運行時拋異常不會顯示出錯的行號,在代碼調試時無法按照源碼行來設置斷點。

LocalVariableTable屬性

用於描述棧幀中局部變量與Java源碼中定義的變量之間的關係,它不是運行時必須的,可以通過javac -g:none取消該信息。如果沒有這個屬性,所有的參數名稱都會丟失,取之以arg0、arg1這樣的佔位符來替代。

其中local_variable_info項代表了一個棧幀與源碼中局部變量的關聯,如下所示:

SourceFile屬性

用於記錄生成這個Class的源碼文件名稱,這個屬性也是可選的。

[圖片上傳失敗...(image-69bee4-1547777034724)]

ConstantValue屬性

作用是通知虛擬機自動爲靜態變量賦值,只有被static關鍵字修飾的變量纔可以用這個屬性。對於非static類型的變量的賦值是在實例構造器<init>方法中進行的;而對於類變量有兩種方式:在類構造器<clinit>方法中或者使用ConstantValue屬性。目前Sun javac編譯器的選擇是:同時使用final和static修飾的變量且爲基本數據類型或String類型使用ConstantValue屬性初始化,否則使用<clinit>初始化。

InnerClass屬性

用於記錄內部類與宿主類之間的關聯。

其中number_of_class代表需要記錄多少個內部類信息,每個內部類的信息都由一個inner_class_info表進行描述。

Deprecated及Synthetic屬性

Deprecated(不推薦使用)和Synthetic(不是由Java源碼直接產生編譯器自行添加的,有兩個例外是實例構造器<init>和類構造器<clinit>)這兩個屬性都屬於布爾屬性,只存在有和沒有的區別,沒有屬性值的概念。在屬性結構中attribute_length的數據值必須爲0x00000000。

StackMapTable屬性

這是一個複雜的變長屬性,位於Code屬性的屬性表中。這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器使用,目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器。

Signature屬性

一個可選的定長屬性,在JDK 1.5發佈後增加的,任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量或參數化類型,則Signature屬性會爲它記錄泛型簽名信息。這主要是因爲Java的泛型採用的是擦除法實現的僞泛型,在字節碼中泛型信息編譯之後統統被擦除,在運行期無法將泛型類型與用戶定義的普通類型同等對待。通過Signature屬性,Java的反射API能夠獲取泛型類型。

BootstrapMethods屬性

一個複雜的變長屬性,位於類文件的屬性表中,用於保存invokedynamic指令引用的引導方法限定符。

字節碼指令簡介

Java虛擬機的指令由一個字節長度的、代表着特定操作含義的數字(操作碼)以及跟隨其後的零至多個代表此操作所需參數(稱爲操作數)而構成。由於Java虛擬機採用面向操作數棧而不是寄存器的架構,所以大多數的指令都不包含操作數,只有一個操作碼。

在指令集中大多數的指令都包含了其操作所對應的數據類型信息,如iload指令用於從局部變量表中加載int類型的數據到操作數棧中。

  • 加載和存儲指令:iload/iload_<n>等(加載局部變量到操作棧)、istore/istore_<n>等(從操作數棧存儲到局部變量表)、bipush/sipush/ldc/iconst_<n>(加載常量到操作數棧)、wide(擴充局部變量表訪問索引);
  • 運算指令:沒有直接支持byte、short、char和boolean類型的算術指令而採用int代替;iadd/isub/imul/idiv加減乘除、irem求餘、ineg取反、ishl/ishr位移、ior按位或、iand按位與、ixor按位異或、iinc局部變量自增、dcmpg/dcmpl比較;
  • 類型轉換指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f;
  • 對象創建與訪問指令:new創建類實例、newarray/anewarray/multianewarray創建數組、getfield/putfield/getstatic/putstatic訪問類字段或實例字段、baload/iaload/aaload把一個數組元素加載到操作數棧、bastore/iastore/aastore將一個操作數棧的值存儲到數組元素中、arraylength取數組長度、instanceof/checkcast檢查類實例類型;
  • 操作數棧管理指令:pop/pop2一個或兩個元素出棧、dup/dup2複製棧頂一個或兩個數組並將複製值或雙份複製值重新壓力棧頂、swap交互棧頂兩個數值;
  • 控制轉移指令:ifeq/iflt/ifnull條件分支、tableswitch/lookupswitch複合條件分支、goto/jsr/ret無條件分支;
  • 方法調用和返回指令:invokevirtual/invokeinterface/invokespecial/invokestatic/invokedynamic方法調用、ireturn/lreturn/areturn/return方法返回;
  • 異常處理指令:athrow
  • 同步指令:monitorenter/monitorexit

公有設計和私有實現

  • Java虛擬機的實現必須能夠讀取Class文件並精確實現包含在其中的Java虛擬機代碼的含義;
  • 但一個優秀的虛擬機實現,通常會在滿足虛擬機規範的約束下具體實現做出修改和優化;
  • 虛擬機實現的方式主要有兩種:將輸入的Java虛擬機代碼在加載或執行時翻譯成另外一種虛擬機的指令集或宿主主機CPU的本地指令集。

Class文件結構的發展

  • Class文件結構一直比較穩定,主要的改進集中向訪問標誌、屬性表這些可擴展的數據結構中添加內容;
  • Class文件格式所具備的平臺中立、緊湊、穩定和可擴展的特點,是Java技術體系實現平臺無關、語言無關兩項特性的重要支柱;

本章小結

本章詳細講解了Class文件結構的各個部分,通過一個實例演示了Class的數據是如何存儲和訪問的,後面的章節將以動態的、運行時的角度去看看字節碼在虛擬機執行引擎是怎樣被解析執行的。

參考資料

原文博客:

《深入理解java虛擬機》筆記3——7種垃圾收集器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章