以前簡單的說了下,如何讀常量池裏面的數據項。java
如今舉個例子,爲你我加深下印象。web
拿 CONSTANT_Class_info
這張表舉例子。它的結構以下:數組
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
第一個數據項是 tag
是標記位的意思,第二個數據項 name_index
是類或者接口的全限定名。它的值是個 u2
類型,換算成十進制,指的就是常量池中的第幾個表。從 1
開始 計算, 0
以前已經說過,被系統設置了。jvm
這個比較特別,常量池中全部的引用,最後都指向一張字符串表,可是不是指向同一張。值得單獨說下。svg
CONSTANT_Utf8_info
的結構以下:工具
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
第一個仍是標記位;第二個是長度,表示從當前開始,日後的 length
個字節就是該表 表示的數據。編碼
常量池結束之後,緊跟着的是 訪問標記 ,佔用兩個字節,用來描述接口或者類的信息,好比,是不是 abstract
,是否被定義 public
仍是 private
,若是是類,是否被修飾爲 final
;3d
一樣的書上給出可一個關係表,列出了對應的關係,好比 ACC_PUBLIC
對應的標誌值爲 0x0001
,表示是否爲 public
;ACC_FINAL
對應的標誌值爲 0x0010
,表示是不是 final
類;ACC_SUPER
對應的標誌值爲 0x0020
, JDK1.2
之後編譯出來的類,這個標誌就爲真;其餘的就不一一列舉了,上面推薦的工具,直接顯示了該類的類型;code
主要說下這個標誌值是怎麼計算的,首先有許多標誌位,只有被用的標誌位纔會被設置爲對應的標誌值,沒用到的,通通設置爲 0
;xml
好比這裏使用到了 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_index
、descriptor_index
,都是索引,指向常量池;
name_index
:簡單名稱,int number();
的簡單名稱就是 number
;
類的 全限定名 ,將 類全名 中的 .
換成 /
;
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
;
上面的例子只是描述字段,描述方法則按照如下順序:
參數列表;參數列表按照參數定義的順序,放在一個 ()
裏面;
返回值 ;
好比 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
屬性 。