1. 程式人生 > >JVM理論:(三/1)class類文件結構

JVM理論:(三/1)class類文件結構

src row classes java 通知 ans 統一 ron 引用

  各種不同平臺的虛擬機,與所有平臺都統一使用的程序存儲格式——字節碼,是構成平臺無關性與語言無關性的基石。

  Java虛擬機不和包括Java在內的任何語言綁定,它只與“Class文件”這種特定的二進制文件格式所關聯。Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符。

  技術分享圖片

  Class文件裏的數據項無論順序還是數量已被嚴格限定,如上圖所示,先看類型這列,從上圖可以看出Class文件只有兩種數據類型,其中的u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節和8個字節,這些是無符號數,可以描述數字、索引引用、數量值或按照UTF-8編碼構成字符串值,無符號數屬於基本的數據類型。而像cp_info、field_info等的這些類型被稱為表,表是由多個無符號數或其他表構成的復合數據類型,都習慣性以“_info”結尾。

  當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若幹個連續的數據項的形式,例如constant_pool_count+constant_pool共同描述的是常量池,用表示之後有constant_pool_count個連續的constant_pool。接下來一一介紹每個屬性。

  winhex軟件可以打開class字節碼,下載鏈接:https://download.csdn.net/download/shenweis/4034852

1、魔數與Class文件的版本

  magic:每個Class文件的頭4個字節稱為魔數,它的唯一作用是確定這個文件是否為一個能被虛擬機接收的Class文件,class

文件的魔數值為0xCAFEBABE

  緊接著魔數的4個字節存儲的是Class文件的版本號,5到6字節是次版本號,7到8字節是主版本號,Java的版本號從45開始,高版本的JDK能向下兼容以前版本的Class文件,但不能向上兼容,例如JDK1.2.2的十進制版本號是46.0,則能支持45.0~46.0的Class文件。

2、常量池

  constant_pool_count:常量池中常量的數量是不固定的,所以在常量池入口需要放置一向u2類型的容量計數值,表示之後有多少常量,這個計數值是從1而不是0開始,即如果constant_pool_count為22,則表示常量池裏有21個常量,第0項用於滿足後面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義。不過Class文件中只有常量池容量計數從1開始,其他都從0開始計數。

  當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址中。

  常量池主要存放兩大類常量:

  字面量 —— 如文本字符串、聲明為final的常量值等。

  符號引用 —— 類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。

  針對以上兩大類,常量池中的常量又可以細分為14種類型的表,如下圖。這14種表都有一個共同的特點,表開始的第一位是一個u1類型的標誌位tag,用來表示當前常量屬於14種類型中的哪種類型。

  技術分享圖片

  常量池結構很繁瑣,因為這14種常量各自均有自己的結構,如下圖所示

  技術分享圖片

  技術分享圖片

  值得一提的是,Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量來描述名稱,所以CONSTANT_Utf8_info型常量的最大長度也就是Java中方法、字段名的最大長度,而CONSTANT_Utf8_info的最大長度就是length的最大值,即u2兩個字節,兩字節的16位二進制碼可以表示的最大值為0xffff,即15*(16^3)+15*(16^2)+15*16+15=65535,所以Java程序中如果定義了超過64KB英文字符的變量或方法名,將會無法編譯。CONSTANT_Utf8_info型常量結構如下圖。

  技術分享圖片

3、訪問標誌

  常量池結束後,緊接著的兩個字節代表訪問標誌(access_flags),這個標誌用於識別一些類或接口層次的訪問信息,包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final等。

  access_flags中一共有16個標誌位可以使用,當前只定義了其中8個,沒有使用到的標誌位要求一律為0。

  技術分享圖片

  16進制看的可能有些疑惑,轉換成2進制就清楚多了。

  技術分享圖片

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

  Class文件通過這三項數據來確定這個類的繼承關系。

  類索引(this_class),用於確定這個類的全限定名。引用一個u2類型的索引值,都是先指向一個類型為CONSTANT_Class_info的類描述符常量,再通過CONSTANT_Class_info類型常量中的索引值找到定義在CONSTANT_Utf8_info類型常量中的全限定名字符串。如下圖。

  技術分享圖片

  父類索引(super_class),用於確定這個類的父類的全限定名,Java不允許多重繼承,所以父類索引只有一個,除了java.lang.Object外,所有Java類都有父類,因此父類索引都不為0。也是引用一個u2類型的索引值,先指向一個類型為CONSTANT_Class_info的類描述符常量,再通過CONSTANT_Class_info類型常量中的索引值找到定義在CONSTANT_Utf8_info類型常量中的全限定名字符串。

  接口索引集合(interfaces),用來描述這個類實現了哪些接口,入口的第一項是一個u2類型的接口計數器,表示接著有多少個u2類型的接口索引,這些被實現的接口將按implements語句後的接口順序從左到右排列在接口索引集合中(如果這個類本身是一個接口,則應當是extends語句)。如果該類沒有實現任何接口,則計數器為0,後面的接口索引集合也不再占用任何字節。

5、字段表集合

  顧名思義字段表(field_info)用於記錄接口或類中聲明的變量的信息,註意是這些字段只包括類級變量、實例級變量,是不包括方法內部聲明的局部變量的。這些被記錄下來的信息包括:字段的作用域(public、private、protected)、是實例變量還是類變量(static)、可變性(final)、並發可見性(volatile、是否強制從主內存讀寫)、可否被序列化(transient)、字段數據類型(基本類型、對象、數組)、字段名稱。

  字段表集合不會列出從父類或父接口中繼承的字段,Java中字段是無法重載的,兩個字段必須是不一樣的名稱(但字節碼中只要兩字段描述符不一致就合法)。

  字段信息入口也是用一個u2類型的容量計數器fields_count來說明這個類有多少個字段,一個字段表的結構如下。

  技術分享圖片

(1)字段訪問標識access_flags也是u2的數據類型,占16位,如下圖。

  技術分享圖片

  實際情況中,類或接口中的類級或實例級變量有以下限制

  • ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個標誌只能選其一;
  • ACC_FINAL、ACC_VOLATILE不能同時選擇;
  • 接口中的字段必須有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL標誌。

(2)name_index、descriptor_index,二者都是對常量池的引用

  name_index代表簡單名稱,簡單名稱是指沒有類型和參數修飾的方法或者字段名稱。

  descriptor_index代表字段的描述符,描述符用來描述字段的數據類型、方法的參數列表(包括數量、類型、以及順序)和返回值。

  基本數據類型(byte、char、double、float、int、long、short、boolean)、void、對象類型的描述符如下圖。

  技術分享圖片

  對象類型的描述符是用L + 對象的全限定名表示,例如String類型的描述符為Ljava/lang/String;。

  數組類型的描述符,每一個維度用一個前置的“[”字符來描述,例如String [][]類型的描述符為[[Ljava/lang/String;,int []的描述符為[I。

  用描述符來描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“()”內,例如,方法void inc()的描述符為()V,方法String toString()的描述符為()Ljava/lang/String;,方法int indexOf(char[] source, int index)的描述符為([CI)I。 

實例講解:

技術分享圖片

  結合上圖與代碼TestClass.class 文件而言,字段表集合從地址 0x000000F8開始:

  • 第一個u2類型的數據為容量計數器fields_count,其值為0x0001,說明這個類只有一個字段表數據。
  • 接下來緊跟的是 access_flags標誌,其值為0x0002,代表private修飾符的ACC_PRIVATE標誌位為真(ACC_PRIVATE標誌的值為0x0002) 。
  • 代表字段名稱的 name_index值為0x0005,查出第5項常量是一個 CONSTANT_Utf8_info類型的字符串,其值為“m”。
  • 代表字段描述符的 descriptor_index 的值為0x0006,指向常量池的字符串“I”。

  根據以上信息,可以推斷出原代碼定義為“private int m;”。

(3)attributes

  屬性表集合用於存儲一些額外的信息,字段都可以在屬性表中描述零至多項的額外信息,單獨作為一點總結。

類變量、成員變量、局部變量這三個分別是在內存的哪分配的?

6、方法表集合

  Class文件存儲中對方法的描述與對字段的描述類似。方法表結構如下圖。

  如果父類方法在子類中沒有被重寫,方法表集合中就不會出現來自父類的方法信息。

  技術分享圖片

(1)access_flags,方法訪問標誌如下圖。

  技術分享圖片

(2)name_index、descriptor_index,與字段表類似,二者都是對常量池的引用,name_index是方法名的常量池引用,descriptor_index是方法的描述符。

(3)attributes,方法的屬性表。

  方法裏的Java代碼,經過編譯器編譯成字節碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性表裏,屬性表在第7點描述。

實例講解  

  技術分享圖片

  • 第一個u2 類型的數據(即計數器容量)的值為0x0002,代表集合中有兩個方法(這兩個方法是編譯器添加的實例構造器<init>和源碼中的方法inc())。
  • 第一個方法的訪問標識值是0x001,也就是只有ACC_PUBLIC 標誌為真。
  • 名稱索引為0x0007,查找常量池表可知對應的方法名是“<init>”。
  • 描述符索引值為 0x0008,對應常量為“()V”。
  • 屬性表計數器 attributes_count的值為0x0001,表示此方法的屬性表集合有一項屬性。
  • 屬性名稱索引為 0x0009,對應常量為“Code”,說明此屬性是方法的字節碼描述。

7、屬性表集合

  前面講過的Class文件、字段表、方法表都出現過屬性表集合,各個屬性表的順序不再嚴格要求。

  屬性表的結構除了attribute_name_index和attribute_length這兩部分是固定的,一共6字節,不同屬性結構不同,所以屬性值的長度固定為整個屬性表長度減去6個字節。屬性表整體結構如下圖。

  attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,代表了該屬性的名稱;attribute_length表示屬性值的長度

  技術分享圖片

Class文件、字段表、方法表都有自己的屬性表,接下來分別舉例

先介紹兩個屬於方法表中的屬性

(1)Code屬性

  Java程序方法體的代碼經過Javac編譯器處理後,最終變為字節碼指令存儲在Code屬性內。但並非所有方法都必須存在這個屬性,像在接口或抽象類中的方法不存在Code屬性。Code屬性表結構如下圖。

  Code屬性是Class文件中最重要的一個屬性,如果把一個Java程序中的信息分為代碼(Code,方法體裏面的Java代碼)和元數據(Metadata,包括類、字段、方法定義及其他信息)兩部分,那麽在整個Class文件中,Code屬性就用於描述代碼,其他數據項都用於描述元數據。

  技術分享圖片

  max_stack代表了操作數棧深度的最大值,虛擬機運行時需要根據這個值來分配棧幀中的操作棧深度。

  max_locals代表了局部變量表所需的存儲空間,單位是Slot,Slot是虛擬機為局部變量分配內容所使用的最小單位,對於byte、char、flaot、int、short、boolean和returnAddress 等長度不超過32位的數據類型,每個局部變量占用1個Slot;而double和long這兩種64位的數據類型則需要兩個Slot來存放。方法參數、顯示異常處理參數、方法體中的局部變量都需要使用局部變量表來存。局部變量表中的Slot可以重用,當代碼執行超過局部變量的作用域時,Solt可以被其他局部變量使用。

  code_length 和 code:用來存儲java 源代碼編譯後生成的字節碼指令。code_length代表方法內存儲字節碼的長度,雖然它是一個u4類型的長度值,但虛擬機中明確限制了一個方法不允許超過65535條字節碼指令,即它實際上只使用了u2的長度,如果超過這個限制, javac編譯器會拒絕編譯。每個code相當於一個字節碼指令,是一個u1類型的單字節,u1的取值範圍是0x00~0xFF,對應十進制為0~255,也就是一共最多能表達256條指令,目前Java虛擬機規範已經定義了其中約200條字節碼指令,通過“虛擬機字節碼指令表”可以查找對應編碼與指令的對應關系。

  exception_table,這個方法的顯示異常處理表集合,異常表對於Code屬性來說並不是必須存在的,格式如下。如果當字節碼在第start_pc行到第end_pc行之間(不含end_pc行)出現了類型為catch_type或者其子類的異常(catch_type為指向一個CONSTANT_Class_info型常量的引用),則轉到第handler_pc行繼續處理。當catch_type的值為0時,代表任意異常情況都需要轉向到handler_pc進行處理。用於try-catch-finally機制。 

  技術分享圖片

(2)Exceptions屬性

  這裏的Exceptions屬性是在方法表中與Code屬性平級的一項屬性,而不是Code屬性表中的異常屬性表。是方法在throws關鍵字後面列舉的異常。結構如下圖。

  表示可能拋出number_of_exceptions種受檢查異常,每一種受檢查異常使用一個exception_index_table項表示,為指向常量池中CONSTANT_Class_info型常量表的索引,代表了該受檢查異常的類型。

  技術分享圖片

(3)LineNumberTable屬性  

  用於描述Java源代碼行號與字節碼行號(字節碼偏移量)之間的對應關系。Code屬性裏的屬性。

(4)LocalVariableTable屬性

  用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的關系,它不是運行時必須的屬性,如果沒有生成這項屬性,最大的影響就是當其它人引用這個方法時,所有參數名稱都丟失,IDE可能會使用諸如arg0、arg1之類的占位符來替換原有的參數名稱。Code屬性裏的屬性。

(5)SourceFile屬性

  用於記錄這生成這個Class文件的源碼文件名稱。類文件裏的屬性。

(6)ConstantValue屬性

  用於通知虛擬機自動為靜態變量賦值。只有被static關鍵字修飾的變量才可以使用這項屬性。字段表的屬性。

  對於實例變量的賦值是在實例構造器<init>方法中進行的;對於類變量,則有兩種式可以選擇:賦值在類構造器<clinit>方法中進行,或者使用ConstantValue屬性來賦值。目前Sun Javac編譯器的選擇是:如果同時使用final和static來修改一個變量,並且這個變量的數據類型是基本類型或java.lang.String的話,就生成ConstantValue屬性來進行初始化,如果這個變量沒有被final修飾,或者並非基本類型或字符串,則選擇在<client>類構造器中進行初始化。

  有ConstantValue屬性的字段必須設置ACC_STATIC標誌,且ConstantValue屬性值只能限於基本類型和String,有這個屬性,則在類加載過程的準備階段,會用ConstantValue中的值來初始化,而不是用默認的值來進行初始化。ConstantValue屬性表結構如下:

  技術分享圖片

  ConstantValue屬性是一個定長屬性,它的attribute_length數據項值必須為2。constantvalue_index數據項代表了常量池中一個字面常量的引用,根據字段類型不同,字面量可以是CONSTANT_Long_info,CONSTANT_Float_info,CONSTANT_Double_info,CONSTANT_Integer_info和CONSTANT_String_info常量中的一種。

(7)InnerClasses屬性

  用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那麽編譯器將會為它及它所包含的內部類生成InnerClasses屬性表。類文件的屬性。

(8)Deprecated及Synthetic屬性

  Deprecated及Synthetic屬性都屬性於標誌類型的布爾值屬性,只存在有和沒有的區別,沒有屬性值的概念。Deprecated屬性用於表示某個類,字段或方法,已經被程序作者定為不再推薦使用,它可以通過代碼中使用@Deprecated註解進行設置。Synthetic屬代表此字段或方法並不是由Java源碼直接產生的,而是由編譯器自行添加的。

(9)Signature屬性

  可以出現在類、字段表、方法表的屬性。記錄類、接口、初始化方法或成員的泛型類型,Java語言的泛型采用的是擦除法實現的偽泛型。在字節碼(code屬性)中,泛型信息編譯之後都通通被擦除掉,好處是實現簡單、運行期能節省一些類型所占的內存空間,壞處是無法將泛型類型與用戶定義的普通類型同等對待,例如運行期做反射時無法獲得泛型信息。

  為了彌補這個缺陷JDK1.5後專門增設的Signature屬性,現在Java的反射API能夠獲取泛型類型,最終的數據來源就是這個屬性。Signature結構如下。

  技術分享圖片

  signature_index值必須是一個對常量池的有效索引,且必須是CONSTANT_Utf8_info結構,如果當前的Signature屬性是類文件的屬性,則這個結構表示類簽名,如果當前的Signature屬性是方法表的屬性,則這個結構表示方法類型簽名,如果當前的Signature屬性是字段表的屬性,則這個結構表示字段類型簽名。

參考鏈接:

  關於哪些字面量會進入常量池的測試https://www.jianshu.com/p/d8492e748c57

  https://blog.csdn.net/ITermeng/article/details/75194686

   https://blog.csdn.net/laiwenqiang/article/details/42043857

  http://gityuan.com/2015/10/17/jvm-class-instruction/

   https://www.cnblogs.com/lrh-xl/p/5350612.html

  https://blog.csdn.net/u012715840/article/details/72792623

   https://blog.csdn.net/zhangqix/article/details/53454406?utm_source=debugrun&utm_medium=referral

JVM理論:(三/1)class類文件結構