1. 程式人生 > >細說JVM(類檔案結構(二))

細說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_indexdescriptor_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_indexattribute_length前面已經介紹過了。

max_stack代表了運算元棧的最大深度。在方法執行的任意時刻,運算元棧都不會超過這個深度。虛擬機器執行時需要根據這個值來分配棧幀中的操作棧深度。

max_locals代表了局部變量表所需要的儲存空間。在這裡,max_locals的單位是slot。方法引數(包括隱式引數this)、顯式異常處理器的引數(try-catch塊中catch塊中定義的異常)以及方法體中定義的區域性變數都需要區域性變量表來存放。需要注意的是,由於區域性變量表中的slot可以重用,所以並不是所有的區域性變數的總slot就是max_locals。編譯器會根據變數的作用域來分配slot給各個變數使用,然後計算max_locals的大小。

code_lengthcode用來儲存位元組碼指令。Java的位元組碼指令的長度都是一個位元組,即最多可以有256個指令,實際上一共有大約200條指令。對於位元組碼指令這裡不過多介紹。

exception_table_lengthexception_table分別是指異常表長度,和異常表集合。

attributes_countattributes是Code屬性中的屬性表集合。

(2)SourceFile屬性

本例子中:
這裡寫圖片描述
SourceFile屬性記錄生成這個Class檔案的原始碼檔名稱。在上面的資料中,0x0001表示屬性表集合中有一個屬性,0x0012(即十進位制18)是屬性名的索引值,查詢常量池可以知道是SourceFile,0x00000002是這個屬性的長度,即兩個位元組,最後的兩個位元組就是這個屬性的內容,是一個常量池索引,0x0013,十進位制19,結果是HelloWorld.java。

到此為止,我們就分析完了一個Class檔案的檔案結構,不過因為例子過於簡單的原因,很多屬性表集合中的屬性都沒有展示,有興趣的可以自己寫一個比較複雜的例子,自己分析一下類檔案結構,有助於提高對於JVM的理解。