學習Java的朋友應該都知道Java從剛開始的時候就打着平臺無關性的旗號,說「一次編寫,處處運行」,其實說到無關性,Java平臺還有另一個無關 性那就是語言無關性,要實現語言無關性,那麼Java體系中的class的文件結構或者說是字節碼就顯得至關重要了,其實Java從剛開始的時候就有兩套 規範,一個是Java語言規範,另一個是Java虛擬機規範,Java語言規範只是規定了Java語言相關的約束以及規則,而虛擬機規範則纔是真正從跨 平臺的角度去設計的。今天咱們就以一個實際的例子來看看,到底Java中一個Class文件對應的字節碼應該是什麼樣子。 這篇文章將首先整體上闡述一下Class到底由哪些內容構成,而後再用一個實際的Java類入手去分析class的文件結構。 java
在繼續以前,咱們首先須要明確以下幾點: 編程
1)Class文件是有8個字節爲基礎的字節流構成的,這些字節流之間都嚴格按照規定的順序排列,而且字節之間不存在任何空隙,對於超過8個字節的數據,將按 照Big-Endian的順序存儲的,也就是說高位字節存儲在低的地址上面,而低位字節存儲到高地址上面,其實這也是class文件要跨平臺的關鍵,由於 PowerPC架構的處理採用Big-Endian的存儲順序,而x86系列的處理器則採用Little-Endian的存儲順序,所以爲了Class文 件在各中處理器架構下保持統一的存儲順序,虛擬機規範必須對起進行統一。 數組
2) Class文件結構採用相似C語言的結構體來存儲數據的,主要有兩類數據項,無符號數和表,無符號數用來表述數字,索引引用以及字符串等,好比 u1,u2,u4,u8分別表明1個字節,2個字節,4個字節,8個字節的無符號數,而表是有多個無符號數以及其它的表組成的複合結構。可能你們看到這裏 對無符號數和表究竟是上面也不是很清楚,不過沒關係,等下面實例的時候,我會再以實例來解釋。 架構
明確了上面的兩點之後,咱們接下來後來看看Class文件中按照嚴格的順序排列的字節流都具體包含些什麼數據: 工具
(上圖來自The Java Virtual Machine Specification Java SE 7 Edition) 學習
在看上圖的時候,有一點咱們須要注意,好比cp_info,cp_info表示常量池,上圖中用 constant_pool[constant_pool_count-1]的方式來表示常量池有constant_pool_count-1個常量,它 這裏是採用數組的表現形式,可是你們不要誤覺得全部的常量池的常量長度都是同樣的,其實這個地方只是爲了方便描述採用了數組的方式,可是這裏並不像編程語 言那裏,一個int型的數組,每一個int長度都同樣。明確了這一點之後,咱們在回過頭來看看上圖中每一項都具體表明瞭什麼含義。 this
1)u4 magic 表示魔數,而且魔數佔用了4個字節,魔數究竟是作什麼的呢?它其實就是表示一下這個文件的類型是一個Class文件,而不是一張JPG圖片,或者AVI的電影。而Class文件對應的魔數是0xCAFEBABE. 編碼
2)u2 minor_version 表示Class文件的次版本號,而且此版本號是u2類型的無符號數表示。 spa
3) u2 major_version 表示Class文件的主版本號,而且主版本號是u2類型的無符號數表示。major_version和minor_version主要用來表示當前的虛擬 機是否接受當前這種版本的Class文件。不一樣版本的Java編譯器編譯的Class文件對應的版本是不同的。高版本的虛擬機支持低版本的編譯器編譯的 Class文件結構。好比Java SE 6.0對應的虛擬機支持Java SE 5.0的編譯器編譯的Class文件結構,反之則不行。 設計
4) u2 constant_pool_count 表示常量池的數量。這裏咱們須要重點來講一下常量池是什麼東西,請你們不要與Jvm內存模型中的運行時常量池混淆了,Class文件中常量池主要存儲了字 面量以及符號引用,其中字面量主要包括字符串,final常量的值或者某個屬性的初始值等等,而符號引用主要存儲類和接口的全限定名稱,字段的名稱以及描 述符,方法的名稱以及描述符,這裏名稱可能你們都容易理解,至於描述符的概念,放到下面說字段表以及方法表的時候再說。另外你們都知道Jvm的內存模型中 有堆,棧,方法區,程序計數器構成,而方法區中又存在一塊區域叫運行時常量池,運行時常量池中存放的東西其實也就是編譯器長生的各類字面量以及符號引用, 只不過運行時常量池具備動態性,它能夠在運行的時候向其中增長其它的常量進去,最具表明性的就是String的intern方法。
5)cp_info 表示常量池,這裏面就存在了上面說的各類各樣的字面量和符號引用。放到常量池的中數據項在The Java Virtual Machine Specification Java SE 7 Edition 中一共有14個常量,每一種常量都是一個表,而且每種常量都用一個公共的部分tag來表示是哪一種類型的常量。
下面分別簡單描述一下具體細節等到後面的實例 中咱們再細化。
6) u2 access_flags 表示類或者接口的訪問信息,具體以下圖所示:
7)u2 this_class 表示類的常量池索引,指向常量池中CONSTANT_Class_info的常量
8)u2 super_class 表示超類的索引,指向常量池中CONSTANT_Class_info的常量
9)u2 interface_counts 表示接口的數量
10)u2 interface[interface_counts]表示接口表,它裏面每一項都指向常量池中CONSTANT_Class_info常量
11)u2 fields_count 表示類的實例變量和類變量的數量
12) field_info fields[fields_count]表示字段表的信息,其中字段表的結構以下圖所示:
上圖中access_flags表示字段的訪問表示,好比字段是public,private,protect 等,name_index表示字段名 稱,指向常量池中類型是CONSTANT_UTF8_info的常量,descriptor_index表示字段的描述符,它也指向常量池中類型爲 CONSTANT_UTF8_info的常量,attributes_count表示字段表中的屬性表的數量,而屬性表是則是一種用與描述字段,方法以及 類的屬性的可擴展的結構,不一樣版本的Java虛擬機所支持的屬性表的數量是不一樣的。
13) u2 methods_count表示方法表的數量
14)method_info 表示方法表,方法表的具體結構以下圖所示:
其中access_flags表示方法的訪問表示,name_index表示名稱的索引,descriptor_index表示方法的描述 符,attributes_count以及attribute_info相似字段表中的屬性表,只不過字段表和方法表中屬性表中的屬性是不一樣的,好比方法 表中就Code屬性,表示方法的代碼,而字段表中就沒有Code屬性。其中具體Class中到底有多少種屬性,等到Class文件結構中的屬性表的時候再 說說。
15) attribute_count表示屬性表的數量,說到屬性表,咱們須要明確如下幾點:
上面說完了Class文件結構中每一項的構成之後,咱們以一個實際的例子來解釋如下上面所說的內容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
packagecom.ejushang.TestClass;
publicclassTestClassimplementsSuper{
privatestaticfinalintstaticVar =0;
privateintinstanceVar=0;
publicintinstanceMethod(intparam){
returnparam+1;
}
}
interfaceSuper{ }
|
經過jdk1.6.0_37的javac 編譯後的TestClass.java對應的TestClass.class的二進制結構以下圖所示:
下面咱們就根據前面所說的Class的文件結構來解析如下上圖中字節流。
1)魔數
從Class的文件結構咱們知道,剛開始的4個字節是魔數,上圖中從地址00000000h-00000003h的內容就是魔數,從上圖可知Class的文件的魔數是0xCAFEBABE。
2)主次版本號
接下來的4個字節是主次版本號,有上圖可知從00000004h-00000005h對應的是0x0000,所以Class的minor_version 爲0x0000,從00000006h-00000007h對應的內容爲0x0032,所以Class文件的major_version版本爲 0x0032,這正好就是jdk1.6.0不帶target參數編譯後的Class對應的主次版本。
3)常量池的數量
接下來的2個字節從00000008h-00000009h表示常量池的數量,由上圖能夠知道其值爲0x0018,十進制爲24個,可是對於常量池的數量 須要明確一點,常量池的數量是constant_pool_count-1,爲何減一,是由於索引0表示class中的數據項不引用任何常量池中的常 量。
4)常量池
咱們上面說了常量池中有不一樣類型的常量,下面就來看看TestClass.class的第一個常量,咱們知道每一個常量都有一個u1類型的tag標識來表示 常量的類型,上圖中0000000ah處的內容爲0x0A,轉換成二級制是10,有上面的關於常量類型的描述可知tag爲10的常量是Constant_Methodref_info,而Constant_Methodref_info的結夠以下圖所示:
其中class_index指向常量池中類型爲CONSTANT_Class_info的常量,從TestClass的二進制文件結構中能夠看出 class_index的值爲0x0004(地址爲0000000bh-0000000ch),也就是說指向第四個常量。
name_and_type_index指向常量池中類型爲CONSTANT_NameAndType_info常量。從上圖能夠看出name_and_type_index的值爲0x0013,表示指向常量池中的第19個常量。
接下來又能夠經過一樣的方法來找到常量池中的全部常量。不過JDK提供了一個方便的工具可讓咱們查看常量池中所包含的常量。經過javap -verbose TestClass 便可獲得全部常量池中的常量,截圖以下:
從上圖咱們能夠清楚的看到,TestClass中常量池有24個常量,不要忘記了第0個常量,由於第0個常量被用來表示 Class中的數據項不引用任何常量池中的常量。從上面的分析中咱們得知TestClass的第一個常量表示方法,其中class_index指向的第四 個常量爲java/lang/Object,name_and_type_index指向的第19個常量值爲<init>:()V,從這裏可 以看出第一個表示方法的常量表示的是java編譯器生成的實例構造器方法。經過一樣的方法能夠分析常量池的其它常量。OK,分析完常量池,咱們接下來再分 析下access_flags。
5)u2 access_flags 表示類或者接口方面的訪問信息,好比Class表示的是類仍是接口,是否爲public,static,final等。具體訪問標示的含義以前已經說過 了,下面咱們就來看看TestClass的訪問標示。Class的訪問標示是從0000010dh-0000010e,期值爲0x0021,根據前面說的 各類訪問標示的標誌位,咱們能夠知道:0x0021=0x0001|0x0020 也即ACC_PUBLIC 和 ACC_SUPER爲真,其中ACC_PUBLIC你們好理解,ACC_SUPER是jdk1.2以後編譯的類都會帶有的標誌。
6)u2 this_class 表示類的索引值,用來表示類的全限定名稱,類的索引值以下圖所示:
從上圖能夠清楚到看到,類索引值爲0x0003,對應常量池的第三個常量,經過javap的結果,咱們知道第三個常量爲 CONSTANT_Class_info類型的常量,經過它能夠知道類的全限定名稱爲:com/ejushang/TestClass /TestClass
7)u2 super_class 表示當前類的父類的索引值,索引值所指向的常量池中類型爲CONSTANT_Class_info的常量,父類的索引值以下圖所示,其值爲0x0004, 查看常量池的第四個常量,可知TestClass的父類的全限定名稱爲:java/lang/Object
8)interfaces_count和 interfaces[interfaces_count]表示接口數量以及具體的每個接口,TestClass的接口數量以及接口以下圖所示,其中 0x0001表示接口數量爲1,而0x0005表示接口在常量池的索引值,找到常量池的第五個常量,其類型爲CONSTANT_Class_info,其 值爲:com/ejushang/TestClass/Super
9)fields_count 和 field_info, fields_count表示類中field_info表的數量,而field_info表示類的實例變量和類變量,這裏須要注意的是 field_info不包含從父類繼承過來的字段,field_info的結構以下圖所示:
其中access_flags表示字段的訪問標示,好比public,private,protected,static,final等,access_flags的取值以下圖所示:
其中name_index 和 descriptor_index都是常量池的索引值,分別表示字段的名稱和字段的描述符,字段的名稱容易理解,可是字段的描述符如何理解呢?其實在JVM 規範中,對於字段的描述符規定以下圖所示:
其中你們須要關注一下上圖最後一行,它表示的是對一維數組的描述符,對於String[][]的描述符將是[[ Ljava/lang/String,而對於int[][]的描述符爲[[I。接下來的attributes_count以及 attribute_info分別表示屬性表的數量以及屬性表。下面咱們仍是以上面的TestClass爲例,來看看TestClass的字段表吧。
首先咱們來看一下字段的數量,TestClass的字段的數量以下圖所示:
從上圖中能夠看出TestClass有兩個字段,查看TestClass的源代碼可知,確實也只有兩個字段,接下來咱們看看第一個字段,咱們知道第一個字段應該爲private int staticVar,它在Class文件中的二進制表示以下圖所示:
其中0x001A表示訪問標示,經過查看access_flags表可知,其爲ACC_PRIVATE,ACC_STATIC,ACC_FINAL,接下 來0x0006和0x0007分別表示常量池中第6和第7個常量,經過查看常量池可知,其值分別爲:staticVar和I,其中staticVar爲字 段名稱,而I爲字段的描述符,經過上面對描述符的解釋,I所描述的是int類型的變量,接下來0x0001表示staticVar這個字段表中的屬性表的 數量,從上圖能夠staticVar字段對應的屬性表有1個,0x0008表示常量池中的第8個常量,查看常量池能夠得知此屬性爲 ConstantValue屬性,而ConstantValue屬性的格式以下圖所示:
其中attribute_name_index表述屬性名的常量池索引,本例中爲ConstantValue,而ConstantValue的 attribute_length固定長度爲2,而constantValue_index表示常量池中的引用,本例中,其中爲0x0009,查看第9個 常量能夠知道,它表示一個類型爲CONSTANT_Integer_info的常量,其值爲0。
上面說完了private static final int staticVar=0,下面咱們接着說一下TestClass的private int instanceVar=0,在本例中對instanceVar的二進制表示以下圖所示:
其中0x0002表示訪問標示爲ACC_PRIVATE,0x000A表示字段的名稱,它指向常量池中的第10個常量,查看常量池能夠知道字段名稱爲 instanceVar,而0x0007表示字段的描述符,它指向常量池中的第7個常量,查看常量池能夠知道第7個常量爲I,表示類型爲 instanceVar的類型爲I,最後0x0000表示屬性表的數量爲0.
10)methods_count 和 method_info ,其中methods_count表示方法的數量,而method_info表示的方法表,其中方法表的結構以下圖所示:
從上圖能夠看出method_info和field_info的結構是很相似的,方法表的access_flag的全部標誌位以及取值以下圖所示:
其中name_index和descriptor_index表示的是方法的名稱和描述符,他們分別是指向常量池的索引。這裏須要結解釋一下方法的描述 符,方法的描述符的結構爲:(參數列表)返回值,好比public int instanceMethod(int param)的描述符爲:(I)I,表示帶有一個int類型參數且返回值也爲int類型的方法,接下來就是屬性數量以及屬性表了,方法表和字段表雖然都有 屬性數量和屬性表,可是他們裏面所包含的屬性是不一樣。接下來咱們就以TestClass來看一下方法表的二進制表示。首先來看一下方法表數量,截圖以下:
從上圖能夠看出方法表的數量爲0x0002表示有兩個方法,接下來咱們來分析第一個方法,咱們首先來看一下TestClass的第一個方法的access_flag,name_index,descriptor_index,截圖以下:
從上圖能夠知道access_flags爲0x0001,從上面對access_flags標誌位的描述,可知方法的access_flags的取值爲 ACC_PUBLIC,name_index爲0x000B,查看常量池中的第11個常量,知道方法的名稱爲<init>,0x000C表示 descriptor_index表示常量池中的第12常量,其值爲()V,表示<init>方法沒有參數和返回值,其實這是編譯器自動生成 的實例構造器方法。接下來的0x0001表示<init>方法的方法表有1個屬性,屬性截圖以下:
從上圖能夠看出0x000D對應的常量池中的常量爲Code,表示的方法的Code屬性,因此到這裏你們應該明白方法的那些代碼是存儲在Class文件方法表中的屬性表中的Code屬性中。接下來咱們在分析一下Code屬性,Code屬性的結構以下圖所示:
其中attribute_name_index指向常量池中值爲Code的常量,attribute_length的長度表示Code屬性表的長度(這裏 須要注意的時候長度不包括attribute_name_index和attribute_length的6個字節的長度)。
max_stack表示最大棧深度,虛擬機在運行時根據這個值來分配棧幀中操做數的深度,而max_locals表明了局部變量表的存儲空間。
max_locals的單位爲slot,slot是虛擬機爲局部變量分配內存的最小單元,在運行時,對於不超過32位類型的數據類型,好比 byte,char,int等佔用1個slot,而double和Long這種64位的數據類型則須要分配2個slot,另外max_locals的值並 不是全部局部變量所須要的內存數量之和,由於slot是能夠重用的,當局部變量超過了它的做用域之後,局部變量所佔用的slot就會被重用。
code_length表明了字節碼指令的數量,而code表示的時候字節碼指令,從上圖能夠知道code的類型爲u1,一個u1類型的取值爲0x00-0xFF,對應的十進制爲0-255,目前虛擬機規範已經定義了200多條指令。
exception_table_length以及exception_table分別表明方法對應的異常信息。
attributes_count和attribute_info分別表示了Code屬性中的屬性數量和屬性表,從這裏能夠看出Class的文件結構中,屬性表是很靈活的,它能夠存在於Class文件,方法表,字段表以及Code屬性中。
接下來咱們繼續以上面的例子來分析一下,從上面init方法的Code屬性的截圖中能夠看出,屬性表的長度爲0x00000026,max_stack的 值爲0x0002,max_locals的取值爲0x0001,code_length的長度爲0x0000000A,那麼00000149h- 00000152h爲字節碼,接下來exception_table_length的長度爲0x0000,而attribute_count的值爲 0x0001,00000157h-00000158h的值爲0x000E,它表示常量池中屬性的名稱,查看常量池得知第14個常量的值爲 LineNumberTable,LineNumberTable用於描述java源代碼的行號和字節碼行號的對應關係,它不是運行時必需的屬性,若是通 過-g:none的編譯器參數來取消生成這項信息的話,最大的影響就是異常發生的時候,堆棧中不能顯示出出錯的行號,調試的時候也不能按照源代碼來設置斷 點,接下來咱們再看一下LineNumberTable的結構以下圖所示:
其中attribute_name_index上面已經提到過,表示常量池的索引,attribute_length表示屬性長度,而start_pc和 line_number分表表示字節碼的行號和源代碼的行號。本例中LineNumberTable屬性的字節流以下圖所示:
上面分析完了TestClass的第一個方法,經過一樣的方式咱們能夠分析出TestClass的第二個方法,截圖以下:
其中access_flags爲0x0001,name_index爲0x000F,descriptor_index爲0x0010,經過查看常量池可 以知道此方法爲public int instanceMethod(int param)方法。經過和上面相似的方法咱們能夠知道instanceMethod的Code屬性爲下圖所示:
最後咱們來分析一下,Class文件的屬性,從00000191h-00000199h爲Class文件中的屬性表,其中0x0011表示屬性的名稱,查看常量池能夠知道屬性名稱爲SourceFile,咱們再來看看SourceFile的結構以下圖所示:
其中attribute_length爲屬性的長度,sourcefile_index指向常量池中值爲源代碼文件名稱的常量,在本例中SourceFile屬性截圖以下:
其中attribute_length爲0x00000002表示長度爲2個字節,而soucefile_index的值爲0x0012,查看常量池的第18個常量能夠知道源代碼文件的名稱爲TestClass.java