一、前言
上一篇部落格的位址:細說JVM(類檔案結構(一))
二、類檔案分析
5、類索引、父類索引與接口索引集合
在通路标志
access_flags
後接下來就是類索引(
this_class
)和父類索引(
super_class
),這兩個資料都是u2類型的,而接下來的接口索引集合是一個u2類型的集合,class檔案由這三個資料項來确定類的繼承關系。由于Java中是單繼承,是以父類索引隻有一個;但Java類可以實作多個接口,是以接口索引是一個集合。
類索引用來确定這個類的全限定名,這個全限定名就是說一個類的類名包含所有的包名,然後使用”/”代替”.”。比如Object的全限定名是java.lang.Object。父類索引确定這個類的父類的全限定名,除了Object之外,所有的類都有父類,是以除了Object之外所有類的父類索引都不為0.接口索引集合存儲了implements語句後面按照從左到右的順序的接口。
類索引和父類索引都是一個索引,這個索引指向常量池中的
CONSTANT_Class_info
類型的常量。然後再
CONSTANT_Class_info
常量中的索引就可以找到常量池中類型為
CONSTANT_Utf8_info
的常量,而這個常量儲存着類的全限定名。
在本例子中:
this_class
的值是0x0005,即十進制的5,指向的
CONSTANT_Class_info
中的索引是26,常量池中索引是26的
CONSTANT_Utf8_info
的常量值是
temp/HelloWorld
。這樣就解析到了這個類的全限定名,類的父類的全限定名也可以這樣解析。
由于這個類沒有實作接口,是以接口索引集合的容量計數是0。如果容量計數是0,就不需要存儲接口的資訊。
6、字段表集合
字段表集合,顧名思義就是Java類中的字段,字段又分為類字段(靜态屬性)和執行個體字段(對象屬性),那麼,在Class檔案中是如何儲存這些字段的呢?我們可以想一想儲存一個字段需要儲存它的哪些資訊呢?
答案是:字段的作用域(public、private和protected修飾符)、是執行個體變量還是類變量(static修飾符)、可變性(final修飾符)、并發可見性(volatile修飾符)、是否可被序列化(transient修飾符)、字段的資料類型(基本類型、對象、數組)以及字段名稱。
這些資訊中,各個修飾符可以用布爾值表示。而字段叫什麼名字、字段被定義為什麼類型資料都是無法固定的,隻能用常量池中的常量來表示。下面是字段表的格式:
其中的字段修飾符
access_flags
,和類中的
access_flags
類似,對于字段來說可以設定的标志位及含義如下:
access_flags
給出了字段中所有可以用布爾值表示的修飾符,剩下的資訊就是字段的名字、變量類型等資訊。
access_flags
後面的是
name_index
和
descriptor_index
,前者是字段名的常量池索引,後者是字段描述符的常量池索引。
name_index
可以描述字段的名字,
descriptor_index
可以描述字段的資料類型。不過,對于方法的描述符來說就要複雜一些,因為一個方法除了傳回值類型,還有參數類型,而且參數的個數還不确定。根據描述符規則,這些類型都使用一個大寫字母來表示,如下表:
對于數組類型,每一個次元将使用一個前置的“[”字元來描述。比如定義一個
java.lang.String[][]
類型的二維數組,将記錄為
[[Ljava/lang/String
,一個double數組
double[]
将标記為
[D
。
當描述符用來描述方法時,按照先參數清單,後傳回值的順序描述,參數清單按照參數的嚴格順序放在一組小括号
()
内。比如方法
void inc()
的描述符是:
()V
。方法
java.lang.String toString()
的描述符是:
()Ljava/lang/String
。方法
int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)
的描述符是:
([CII[CIII)I
。
descriptor_info
後面是屬性資訊,這會在後面屬性表集合中介紹。
在本例子中:
額,因為在程式中根本沒有定義什麼字段,是以字段的數量是0x0000,是以就是0,也就沒有什麼字段表,不過這都不是什麼問題,下面我們看一下方法表。
7、方法表集合
在字段表集合中介紹了字段的描述符和方法的描述符,對于了解方法表有很大幫助。class檔案存儲格式中對方法的描述和對字段的描述幾乎相同,方法表的結構也和字段表相同,這裡就不再列出。不過,方法表的通路标志和字段的不同,列出如下:
本例子中:
我們可以看出,前兩個位元組是方法表集合中的院元素個數,這裡是0x0002,是以有兩個方法,按照字段的解析方法,可以得到每個方法的定義。分别是:
-
public <init> ()V
-
public static main ([Ljava/lang/String;)V
但是我們發現我們本來隻定義了一個
main
方法,為啥會有兩個方法呢?
其實,Java類都要有一個構造方法,如果沒有的話編譯器會自動構造一個無參的構造方法,就是上面的第一個名叫
<init>
的方法;同時,如果一個類中含有靜态代碼塊或者靜态變量,那麼就需要首先執行類的構造方法,來執行靜态代碼塊和初始化靜态變量,但是上面的代碼中并沒有靜态變量,也沒有靜态代碼塊,是以也就沒有虛拟機預設添加的
<clinit>
的方法。
不過,方法比字段還多了方法體呢,那方法體中的代碼哪去了?
在每一個方法表中
descriptor_index
後描述屬性的時候,0x0001表明屬性的個數為1,再後面的0x0009是指向常量池中的
CONSTANT_Utf8_info
常量,内容是Code,說明後面屬性中存放的就是方法體裡面編譯後的位元組碼指令。
8、屬性表集合
屬性表在前面出現了多次,在Class檔案、字段表和方法表都可以攜帶自己的屬性表集合,來描述某些場景專有的資訊。
與Class檔案中其他的資料項目要求嚴格的順序、長度和内容不同,屬性表集合的限制比較少,不要求嚴格的順序,隻要不與已有的屬性名重複,任何人實作的編譯器都可以向屬性表中寫入自定義的屬性資訊,Java虛拟機會在運作時忽略掉那些不認識的資訊。為了能正确解析class檔案,《Java虛拟機規範(第二版)》中預定義了9項虛拟機應當識别的屬性。現在,屬性已經達到了21項。具體資訊如下表,這裡僅對常見的屬性做介紹:
從上表可以看出,屬性表集合存在的位置也是不确定的,不僅可以存儲在Class檔案結尾處,還可以作為資料項存在于類、方法表集合和字段表集合、Code屬性中。對于存在于Class類檔案中的屬性表集合很好了解,畢竟在開頭的Class檔案結構圖中的最後一部分就是屬性表集合,這時屬性表集合作為構成Class檔案結構的一個大部分。剩下的存在于類中、方法表集合與字段表集合和Code屬性中的屬性表集合,其實是作為它們的一個資料項存在的。
存在于類中的屬性表集合,存儲了關于這個類的一些資訊。比如這個類是否是過時的(Deprecated)、在泛型中儲存類的類型參數(由于生成Class檔案後會進行類型擦除,Java中的泛型是一種僞泛型)和動态注解等資訊;存放在方法表集合中的屬性表集合存儲了關于方法的資訊,最主要的就是Code屬性,存儲了位元組碼指令;存放于字段表集合中的屬性表集合存儲了關于字段的資訊,我們這裡的例子沒有涉及到字段的屬性,不過當在類中定義了靜态常量(static final)并且這個常量有初始值時會将這個值作為屬性存儲在字段表中的屬性表集合中。
由于屬性表集合的限制較小,每個屬性都會有自己的格式,是以class檔案對于屬性的格式要求也比較寬松,隻需要滿足一些特定的條件即可。下表是屬性的結構:
從上表可以看出,Class檔案規定的屬性格式隻有前6個位元組:兩個位元組的屬性名稱的索引和4個位元組的屬性長度,接下來就要按照這個長度存儲屬性值了。這樣的寬松格式使得屬性表的結構可以多樣變化,甚至可以在屬性的内容中再加入一個屬性,比較常用的就是方法表集合中的Code屬性,在Code屬性中還有LineNumberTable屬性和LocalVariableTable屬性等。
接下來就簡單介紹一下常用的屬性。
(1)Code屬性
最常用的屬性恐怕就是Code屬性了,因為大多數的方法都會有編譯後的位元組碼指令,這些指令就存儲在方法表中的Code屬性中。如果一個Java程式的資訊可以分為代碼(方法體中的代碼)和中繼資料(包括類、字段、方法定義以及其它資訊),那麼Code屬性存儲的就是代碼,其它所有的結構存儲的都是中繼資料。不過并非所有的方法表都有這個Code屬性,比如接口或抽象類中的方法表就不存在Code屬性,Code屬性的結構如下::
其中
attribute_name_index
和
attribute_length
前面已經介紹過了。
max_stack
代表了操作數棧的最大深度。在方法執行的任意時刻,操作數棧都不會超過這個深度。虛拟機執行時需要根據這個值來配置設定棧幀中的操作棧深度。
max_locals
代表了局部變量表所需要的存儲空間。在這裡,
max_locals
的機關是
slot
。方法參數(包括隐式參數this)、顯式異常處理器的參數(try-catch塊中catch塊中定義的異常)以及方法體中定義的局部變量都需要局部變量表來存放。需要注意的是,由于局部變量表中的
slot
可以重用,是以并不是所有的局部變量的總
slot
就是
max_locals
。編譯器會根據變量的作用域來配置設定
slot
給各個變量使用,然後計算
max_locals
的大小。
code_length
和
code
用來存儲位元組碼指令。Java的位元組碼指令的長度都是一個位元組,即最多可以有256個指令,實際上一共有大約200條指令。對于位元組碼指令這裡不過多介紹。
exception_table_length
和
exception_table
分别是指異常表長度,和異常表集合。
attributes_count
和
attributes
是Code屬性中的屬性表集合。
(2)SourceFile屬性
本例子中:
SourceFile屬性記錄生成這個Class檔案的源碼檔案名稱。在上面的資料中,0x0001表示屬性表集合中有一個屬性,0x0012(即十進制18)是屬性名的索引值,查找常量池可以知道是SourceFile,0x00000002是這個屬性的長度,即兩個位元組,最後的兩個位元組就是這個屬性的内容,是一個常量池索引,0x0013,十進制19,結果是HelloWorld.java。
到此為止,我們就分析完了一個Class檔案的檔案結構,不過因為例子過于簡單的原因,很多屬性表集合中的屬性都沒有展示,有興趣的可以自己寫一個比較複雜的例子,自己分析一下類檔案結構,有助于提高對于JVM的了解。