類檔案結構_class類檔案的的結構

z1340954953發表於2018-04-11

跨平臺的實現

Java誕生之初提出一個口號"一次編寫,到處執行"。與平臺無關的思想最終實現在作業系統的應用層上:Sun公司以及其他虛擬機器提供商釋出了許多可以執行在各種不同平臺上的虛擬機器,這些虛擬機器都可以載入和執行同一種平臺無關的位元組碼,從而實現一次編寫,到處執行。

實現語言無關性(java虛擬機器上可以執行Scala等其他語言)仍然是虛擬機器和位元組碼的儲存格式.

java 虛擬機器不和任何語言繫結,只是和Class檔案這種特定的二進位制檔案格式關聯。Class檔案包含了java虛擬機器指令集和符號表,等資訊.也就是java虛擬機器可以支援其他語言,只要能編譯為class格式的檔案就行

Class 檔案的 結構

任何一個Class檔案都對應著唯一一個類或者介面的資訊,但是反過來說類或者介面的資訊並不一定都得定義在檔案中(類或者介面的資訊也能夠通過類載入器生成),任意一個有效的類或介面應當滿足的格式稱為"Class檔案格式",並不一定以磁碟檔案存在

Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊排列在Class檔案之中,沒有任何分隔符

,如果佔用的空間不夠,則會按照big-endian分隔為多個8位位元組進行儲存

根據Java虛擬機器規範的規定,Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構中只有兩種資料型別無符號數和表。

無符號數屬於基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串值

表是有多個無符號數或者其他表作為資料項構成的複合資料型別,所有表都習慣地以_info結尾。表用於描述有層次關係的複合結構的資料,整個class檔案本質上就是一張表

下面看下class檔案的一個結構圖


最後,強調下, 由於Class的結構不像xml等描述語言,沒有任何的分隔符,所以class檔案結構中的資料項中,無論是順序還是數量,甚至是儲存的位元組序(Class檔案中儲存的位元組序是big-endian),都是嚴格規定的,下面按照這表來介紹下檔案結構

魔數和class檔案的版本號

從上面的表看出,class檔案的頭4個位元組為magic,魔數,確定這個檔案是否是java虛擬機器可以接受的class檔案,後面的4個位元組,minor_version表示的是次版本號,major_version表示的是主版本號

常量池

從上面的表看出,後面存放的常量計數器和表(也就是常量池),常量池中主要儲存兩大類常量:

字面量(Literal)和符號引用(Symbolic References),字面量比較接近於java語言常量概念,如文字字串、宣告為final的常量值等。而符號引用則屬於編譯原理方面的概念,包括了三類常量:

1> 類和介面的全限定名

2> 欄位的名稱和描述符

3> 方法的名稱和描述符

java程式碼在進行javac編譯的時候,是在虛擬機器載入Class檔案的時候進行動態連線,也就是說,Class檔案中不會儲存各個方法、欄位的最終記憶體佈局資訊,如果不經過執行期轉換無法得到真正的記憶體入口地址。

也就是當虛擬機器執行時候,需要從常量池中獲取對應的符號引用,再在類建立時或執行時解析翻譯找到具體的記憶體地址

什麼是符號引用,什麼是直接引用?

符號引用:符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能定位到目標即可,符號引用和虛擬機器的記憶體佈局無關,符號引用存在引用的目標不一定載入到了記憶體中

直接引用:直接引用是一個能定位到目標的指標、偏移量或者控制程式碼。直接引用和虛擬機器的記憶體佈局相關,只要直接引用存在,目標一定載入到了記憶體中

常量池的每一個常量都是一個表,jdk1.7中新增了3個型別,常量池的常量型別如下:


在CONSTANT_Class_info中name_index是一個索引值,指向常量池中的一個CONSTANT_Utf8_info型別常量,這個常量表示類或者介面的全限定名,這裡name_index值:0x0002,指向了常量池中第二個常量,就是CONSTANT_Utf8_info,標識位是0x01,由此可以確定name_index是指向常量池中的CONSTANT_Utf8_info型別常量


length值說明了這個UTF-8編碼的字串長度是多少個位元組,後面緊跟的長度為length位元組的連續資料是一個使用utf-8略碼標識的字串。

utf-8縮略編碼和普通utf-8編碼的區別是:從 \u001到\u007的字元(相當於1~127的ASCII碼)的縮略編碼使用一個位元組表示,從\u0080到\u07ff之間的字元使用2個位元組表示

由於Class檔案中方法、欄位等都需要引用CONSTATN_Utf8_info描述常量,這個常量的最大長度是length的長度,u2型別最大值為65535,java程式中如果定義了超過了64kb的變數或者方法名,將無法編譯

我們使用javap來看下Test類編譯位元組碼資訊,class檔案中常量池的資訊如下:

d:\>javap -verbose Test.class
Classfile /d:/Test.class
  Last modified 2018-4-12; size 275 bytes
  MD5 checksum 79ebe8a03c66bed1910b536bcec097ee
  Compiled from "Test.java"
public class com.test.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/test/Test.m:I
   #3 = Class              #17            // com/test/Test
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               getM
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               com/test/Test
  #18 = Utf8               java/lang/Object
{
從這裡看出有18個常量資訊,#表示儲存的是索引資訊(utf8表示索引指向的是字串資訊),後面的表示指向的內容

訪問標誌

常量池後面的兩個位元組表示的是訪問標誌,這個標誌用於識別一些類或者介面層次的訪問資訊,包括這個class是類還是介面,是否定義為public型別;是否定義為abstract型別,如果是類的話,是否定義為final


如果有一個普通的java類,是public,但是不是final和abstract的,這樣只有上面的兩個標誌位

acc_public ,acc_super ,結果就是這個類的標識位access_flags = 0x0021

類索引、父類索引與介面索引集合

類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合(interfaces)是一組u2型別的資料的集合,Class檔案中由這三項資料來確定這個類的繼承關係。

欄位表集合

欄位表用於描述介面或者類中宣告的變數。欄位包括類級變數以及例項級變數,但是不包括在方法內部宣告的區域性變數。

包括的資訊有:欄位的作用域,是例項變數還是類變數,可變性(final),併發可見性(volatile修飾符,是否強制從主記憶體中讀寫),是否可以被序列化(transient修飾符),欄位資料型別、欄位名稱。

而欄位名稱、欄位的型別需要引用常量池中的符號引用來描述


access_flags和類檔案結構中的含義類似,表示訪問標誌,是否是public,abstract,final等

name_index:常量池中名稱的引用,代表欄位的簡單名稱

descriptor_index:常量池中描述的引用,代表欄位和方法的描述符

通過name_index和descriptor_index就可以確定,欄位名和描述符 比如 private int m 的資訊 

解釋下一些名詞,簡單名稱,方法和欄位的描述符

例如:

全限定名:org/fengxiao/test/Mytest 就是這個類的全限定名

方法test(),int m =0;欄位的簡單名稱就是 test,m

描述符的作用:描述欄位的資料型別、方法的引數列表和返回值,基本型別、void和物件型別都使用一個字母表示

   

注意:

1. 對於陣列型別,每一個維度將使用一個前置的[符號來描述,如定義為java.lang.String[][]型別的陣列,將會被記錄為[[Ljava/lang/String,一個整型陣列int[] 將記錄為[I 

2. 用描述符來描述方法時候,按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組小括號()內,

例如:void () 描述符表示為 ()V

         java.lang.String toString() 描述符表示為()L java/lang/String

        int get(int m,String flag, double n) 描述符為(ILD)I

注意

1. 欄位表集合不會列出從父類中繼承而來的欄位,但是可能列出原來java程式碼中不存在的欄位,比如內部類中為了保持

對外部類的訪問性,會自動新增外部類的例項欄位

2. 在java語言中欄位是無法過載的,兩個欄位的資料型別、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於位元組碼,如果兩個欄位的描述符不同,欄位是允許重名的

方法表集合

方法表的結構如同欄位表一樣,依次包括了訪問標誌,名稱索引,描述符索引、屬性表集合


方法的定義可以通過訪問標誌、名稱索引、描述符所以來表示,但是裡面的程式碼去哪了?方法裡的java程式碼,經過編譯器編譯恆位元組碼指令後,存放在方法屬性表集合中一個名為"code"的屬性裡面

與欄位表集合相對應的,如果父類方法在子類中沒有被重寫,方法表集合中就不會出現來自父類的方法資訊,但是可能出先有編譯器自動新增的方法,最典型的就是類構造器<clinit>和例項構造器<init>方法

在java語言中,要過載Overload一個方法,除了要和原方法具有相同的簡單名稱之外,還要求必須擁有和原方法不同的方法簽名,特徵簽名就是一個方法中各個引數在常量池中的欄位符號引用的集合,因為返回值不包含在特徵簽名,因而無法通過返回值不同完成過載

在class檔案格式中,特徵簽名的範圍更大些,只要描述符號不是一致的兩個方法也可以共存,比如,如果兩個方法具有相同的名稱和特徵簽名,返回值不同,也是可以共存在一個class檔案中,只不過不是稱為過載

屬性表 attribute_info

屬性表在class檔案中、欄位表、方法表中用來描述一些特殊場景下的資訊

屬性表中不像class檔案中其他結構一樣,有嚴格的順序要求,只要不和已有的屬性名重名,虛擬機器中識別的部分屬性表如下:

虛擬機器規範預定義的屬性
屬性名稱使用位置含義
Code方法表java程式碼編譯成的位元組碼指令
ConstantValue 欄位表final關鍵字定義的常量值
Deprecated類、方法表、欄位表被宣告為deprecated的方法和欄位
Exception方法表方法丟擲的異常
EnclosingMethod類檔案僅在一個類為區域性類或者匿名類是才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法
InnerClasses類檔案內部類列表
LineNumberTableCode屬性java原始碼行號和位元組碼指令的對應關係
LocalVariableTableCode屬性方法的區域性變數描述
StackMapTableCode屬性jdk1.6中新增的屬性,供新的型別檢查驗證器檢查和處理目標方法的區域性變數和運算元棧所需要的型別是否匹配
Synthetic類、方法表、欄位表標識方法或欄位為編譯器自動生成的

每個屬性,名稱也是需要從常量池中引用一個COSNTANT_Utf8_info型別的常量來表示,屬性值的結構是完全自定義的,只需要一個u4長度屬性說明屬性值佔的位數即可,一個符合規則的屬性表應該滿足下面結構

Code屬性表的結構
型別名稱數量
u2attribute_name_index1
u4attribute_length1
u1info    attribute_length

1. code 屬性

java方法編譯為位元組碼後,放在code屬性中,code屬性放在方法表的方法集合中,並非所有的方法表都存在code屬性,介面和抽象類方法表不存在code屬性,code屬性的結構表示如下:

屬性表的結構
型別名稱數量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attribute_count1
attribute_infoattributesattribute_count

*attribute_name_index

指向常量池中CONSTANT_Utf8_info型別的常量的引用,表示名稱,固定是"Code"

*max_stack 

表示最大的運算元棧的深度,java虛擬機器執行時,根據這個來分配每個棧幀的運算元棧的深度

*max_local

表示區域性變數表儲存的空間,單位是slot

對於byte,short,int,float等不超過32位資料型別使用一個slot來儲存,long,double超出了32位,使用兩個slot儲存

 並不是方法區中用到多少區域性變數,就將這些變數的總和,作為max_local,因為slot是可以重用的

*code_length、code:

用來儲存java原始碼編譯後生成的位元組碼指令,code_length代表位元組碼長度,code是儲存位元組碼指令的一些列位元組流

位元組碼指令就是一個u1型別的單位元組,當虛擬機器讀取到code中的一個位元組碼時,就可以對應找出這個位元組碼代表的是什麼指令

對於code_length雖然是U4型別的,但是實際上虛擬機器規定了方法不能超過65534KB位元組,一般情況下,只要不是人為將方法寫的很長是不會超過65KB的限制的,但是翻譯jsp檔案的情況下, 將頁面內容放入一個方法內,就可能超出這個限制

note: Code屬性是Class檔案中重要屬性,如果將一個java程式中的資訊分為程式碼(方法體裡面的java程式碼)和後設資料(Metadata,包括類、欄位、方法定義及其他資訊)。


上圖是例項構造器<init>方法的Code屬性,可以看出code位元組碼為 2A B7 00 0A B1,一個位元組一個位元組翻譯過去就是:

1> 讀取2A,查表的0x2A對應的指令為aload_0,這個指令含義是將第0個Slot中為reference型別的本地變數推送到棧頂

2> 讀取B7,查表的0xB7對應的指令為invokespecial,這條指令的作用是以棧頂的reference型別指定的物件作為方法的接受者,呼叫此物件的例項構造方法、private方法或者父類的方法

3> 讀取 00 0A ,這是invokespecial的引數,查常量池的0x000A對應的常量為例項構造器方法的引用

4> 讀取B1,查表0xB1對應的指令為return,含義是返回此方法,返回值是void

TestClass.java

package com.test;  
  
public class TestClass {  
    private int m;  
      
    public int inc(){  
        return m + 1;  
    }  
}  

javap指令翻譯的位元組碼:

public com.test.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<ini
":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 7: 0
}

Inc方法中方法引數和區域性變數都沒有,那麼locals,和arg_size為什麼是1?

在任何例項方法裡面,都可以通過this關鍵字訪問到此方法所屬的物件,實現原理是通過javac編譯器編譯的時候把對this關鍵字的訪問轉為對一個普通方法引數的訪問,然後虛擬機器呼叫例項方法時候自動傳入此引數。

因而在例項方法的區域性變數表中至少會存在一個指向當前物件例項的區域性變數,區域性變數表中也會存在一個slot位來存放例項引用,方法引數值從1開始計算,如果將Inc方法改為static,那麼args_size就是0了

異常表Exception

異常表結構
型別名稱數量型別名稱數量
u2start_pc1u2handler_pc1
u2end_pc1u2catch_pc1

當位元組碼在start_pc到end_pc之間,出現了型別為catch_type(是指向常量池中型別為CONSTANT_Utf8_info的引用)或子類的異常,就會轉到handler_pc執行,當catch_pc值為0,表示任何情況都要轉到handler_pc處理

java程式碼:

 public int inc(){  
      int x;
      try{
		  x=1;
		  return x;
	  }catch(Exception e){
		  x=2;
		  return x;
	  }finally{
		  x=3;
	  }
    }  

javap -verbose TestClass.clas 檢視位元組碼執行:

public int inc();
   descriptor: ()I
   flags: ACC_PUBLIC
   Code:
     stack=1, locals=5, args_size=1
        0: iconst_1 //try中x=1
        1: istore_1
        2: iload_1 //儲存x 到returnValue中,此時x=1
        3: istore_2
        4: iconst_3 //finally塊中x=3
        5: istore_1
        6: iload_2 //將returnValue中的值放到棧頂,準備給ireturnn返回
        7: ireturn
        8: astore_2 //給catch中定義的Exception e賦值,儲存在slot2中
        9: iconst_2 //catch塊中x=2
       10: istore_1
       11: iload_1 // 將x儲存到returnValue中,此時x=2
       12: istore_3
       13: iconst_3 
       14: istore_1
       15: iload_3
       16: ireturn
       17: astore        4
       19: iconst_3
       20: istore_1
       21: aload         4
       23: athrow
     Exception table:
        from    to  target type
            0     4     8   Class java/lang/Exception
            0     4    17   any
            8    13    17   any
           17    19    17   any
     LineNumberTable:
       line 9: 0
       line 10: 2
       line 15: 4
       line 10: 6
       line 11: 8
       line 12: 9
       line 13: 11
       line 15: 13
       line 13: 15
       line 15: 17
     StackMapTable: number_of_entries = 2
       frame_type = 72 /* same_locals_1_stack_item */
         stack = [ class java/lang/Exception ]
       frame_type = 72 /* same_locals_1_stack_item */
         stack = [ class java/lang/Throwable ]

SourceFile: "TestClass.java"

Exception屬性

這裡的Exception屬性是在方法表中和Code屬性平級的一項屬性,不和前面的異常表混淆,Exception屬性的作用和列出可能丟擲的受檢查異常,也就是throws關鍵字後面列舉的異常

相關文章