類文件結構的故事(二)


讀常量池的方法

以前簡單的說了下,如何讀常量池裏面的數據項。java

如今舉個例子,爲你我加深下印象。web

CONSTANT_Class_info 這張表舉例子。它的結構以下:數組

類型 名稱 數量
u1 tag 1
u2 name_index 1

第一個數據項是 tag 是標記位的意思,第二個數據項 name_index 是類或者接口的全限定名。它的值是個 u2 類型,換算成十進制,指的就是常量池中的第幾個表。從 1 開始 計算, 0 以前已經說過,被系統設置了。jvm


UTF-8編碼的字符串表

這個比較特別,常量池中全部的引用,最後都指向一張字符串表,可是不是指向同一張。值得單獨說下。svg

CONSTANT_Utf8_info 的結構以下:工具

類型 名稱 數量
u1 tag 1
u2 length 1
u1 bytes length

第一個仍是標記位;第二個是長度,表示從當前開始,日後的 length 個字節就是該表 表示的數據。編碼


訪問標記

常量池結束之後,緊跟着的是 訪問標記 ,佔用兩個字節,用來描述接口或者類的信息,好比,是不是 abstract ,是否被定義 public 仍是 private ,若是是類,是否被修飾爲 final3d

一樣的書上給出可一個關係表,列出了對應的關係,好比 ACC_PUBLIC 對應的標誌值爲 0x0001 ,表示是否爲 publicACC_FINAL 對應的標誌值爲 0x0010 ,表示是不是 final 類;ACC_SUPER 對應的標誌值爲 0x0020JDK1.2 之後編譯出來的類,這個標誌就爲真;其餘的就不一一列舉了,上面推薦的工具,直接顯示了該類的類型;code

在這裏插入圖片描述
主要說下這個標誌值是怎麼計算的,首先有許多標誌位,只有被用的標誌位纔會被設置爲對應的標誌值,沒用到的,通通設置爲 0xml

好比這裏使用到了 final,super,public,所以,三個標誌的標誌值進行 | 運算:0x0001 | 0x0020 | 0x0010 = 0x0031 也就是圖中工具直接算出來的 0x0031


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

類索引、父類索引,都是一個 u2 類型的數據項,接口索引集合,從名字上就能夠看出是個集合,是一組 u2 類型數據的集合;

Class文件,經過這三個數據項,進行肯定繼承關係;

類索引,用於肯定該類、接口的全限定名;

父類索引,用於肯定該類、接口的父類的全限定名,除了 java.lang.Object ,全部的 Java 類的父類索引都不能夠爲 0

接口索引集合,用於描述該類實現了哪些接口|接口繼承了哪些接口,這些接口按照 implement、extend 後面從左到右的順序,排列到接口索引集合中;

類索引、父類索引,都是一個 u2 類型的數據項,都指向一個 CONSTANT_Class_info ,而後 CONSTANT_Class_info 的索引指向一個 CONSTANT_Utf8_info ,裏面保存着對應的 全限定名 ;

接口索引集合,因爲是一個集合,全部跟常量池同樣,首先有個入口,是一個 u2 類型的接口數量計數器,而後後面纔是真正的集合;若是沒有實現或繼承任何接口,則計算器的值爲 0 ,後面接口的索引表將再也不佔用任何字節,也就是不存在;

都是保存着索引的,拿着索引去常量池裏面尋找對應的數據;


字段表集合

字段表用於描述接口或類中申明的變量;

這個變量是類中,不論是不是 static 修飾的,也就是類級別的、實例級別的 ,都算,可是方法內部的不算;

保存的信息,變量的修飾符、名字 ;其中修飾符,有數據類型修飾符(int、douple...),做用域修飾符(private、public...)、static、final、transient、volatile… ;

上面那些修飾符,除了數據類型修飾符,其餘的修飾符,都是要麼幾選一,要麼有,要麼沒有,都是布爾類型,很適合用一個標記位來表示;至於其餘修飾符、類型是千奇百怪的,反正沒法想到怎麼用一個固定字節來表示,那咱們就用字符串記錄下,字符串放進常量池裏面,這裏僅保存引用 ;

字段表的入口,是一個 u2 字節的計數器;

字段表結構以下:

名稱 類型 數量
access_flags u2 1
name_index u2 1
descriptor_index u2 1
attributes_count u2 1
attributes attributes_info attributes_count

除了數據類型的字段的修飾符放到 access_flags訪問標記 基本同樣;

access_flags 以後,就是 name_indexdescriptor_index ,都是索引,指向常量池;

  1. name_index :簡單名稱,int number(); 的簡單名稱就是 number

  2. 類的 全限定名 ,將 類全名 中的 . 換成 /

  3. descriptor_index:描述符,字段的數據類型,方法的參數列表(順序不可亂)和返回值,這些就是字段與方法的描述符;

    • 基本數據類型和 void 都用一個大寫字母表示,對象類型使用 L 加上對象的全限定名錶示
標識字符 被標識的類型
B byte
C char
D double
f float
I int
J long
S short
Z boolean
V void
L 對象類型

數組,什麼類型的數組以及幾維數組,就在對應的類型的標識字符前面添加對應的 [,好比 int[][] number 則記作 [[I ;

上面的例子只是描述字段,描述方法則按照如下順序:

  1. 參數列表;參數列表按照參數定義的順序,放在一個 () 裏面;

  2. 返回值 ;

    好比 public String toString(char c,int[] number) ; 的描述符爲 (C[I)Ljava/lang/String

這以後就是額外信息描述計數器和額外信息描述,後面 屬性表 那裏再講;字段的值就是放在那裏面,咱們這裏說的都是字段的描述:修飾符、名字等等

字段表集合不會列出父類的字段,可是可能列出一些不存在的字段,好比 內部類的屬性中可能會出現外部類引用 ,內部類能夠直接訪問外部類,那麼外部類的可供內部類訪問的字段,會被直接添加到內部類的字節碼的字段表集合中的;


方法表集合

基本上和 字段表集合 同樣;

就再也不重複講了;

主要說下,方法的修飾符、名字、描述符,都被保存起來了;那麼方法體呢,被保存在哪裏呢?方法體通過編譯之後,變爲字節碼指令,存放在 屬性表 集合中的 Code 屬性裏面 ;

一樣父類的方法,若是沒有被子類重寫,是不會被放到該集合中的;

可是集合中可能出現一些編譯器本身添加進去的方法,好比類構造器方法 <clinit> 和實例構造器方法 <init>


插播: 爲何重載,不能根據方法返回值進行區別

java 虛擬機規範裏面,指定 方法特徵簽名,僅僅包括了 方法名稱、參數順序以及類型 ;

各大廠商實現的虛擬機,都按照這個規範來的,在尋找方法的時候,僅僅按照方法特徵簽名來尋找的,而方法特徵簽名裏面,沒有返回值;所以,不能經過方法返回值進行區分重載的方法;

而在字節碼文件裏面,只要描述不是徹底一致的方法,是能夠共存的,也就是能夠經過方法返回值進行區分,只是 jvm規範 中沒有將方法返回值算進去,其實技術層面是能夠經過返回值進行區分的 ;

所以,虛擬機平臺上的其餘語法在實現的時候,能夠將這個歸入進去,轉成字節碼的時候 JVM 是認識的,可是 java 裏面是沒法作到了。


屬性表集合

屬性表,和前面說的那些不太同樣,它在好多地方都有出現,不是一個單獨做爲一項出現的,也就是它能夠出現屢次,不似以前的那些數據項只能在規定的地方出現一次。

前面講的那些,某些數據項中存在屬性表屬性,其中有: Class文件字段表方法表 ,這三個數據項都有本身的屬性表,用於描述一些信息。

屬性表自己也是一個表結構,其結構以下:

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

屬性表的限制不像其餘表和雙規似的限制那麼嚴格,它對其中的屬性沒有順序要求,甚至對屬性的個數也沒有要求,任何人均可以往屬性表裏面添加屬性,就是你添加的屬性,JVM 不認識,在運行的時候會被自動忽略掉而已。

爲了能正確的解析 Class 文件,解析裏面的屬性表的屬性,JVM 規範目前制定了 21 個屬性,用於描述特定場景的信息。 其中每一個屬性自己也是一張表。

其中有些屬性,只能用在 方法表或者字段表或者類文件中,也有能夠三個通用的屬性,也有能夠用在其餘屬性的屬性。通常是一些屬性能夠用在 Code 屬性 。

相關文章
相關標籤/搜索