1. 程式人生 > >java類檔案結構

java類檔案結構

Class檔案是一組8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全部都是程式執行的必要資料,沒有空隙存在。當遇到需要佔用8位位元組以上空間的資料項時,則會按照高位在前的方式分割成若干個8位位元組進行儲存。 根據Java虛擬機器規範的規定,Class檔案格式採用一種類似於C語言結構體的偽結構來儲存的,這種偽結構中只有兩種資料型別:無符號數和表無符號數屬於基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值,或者按照UTF-8編碼構成字串值。 表是由多個無符號數或其他表作為資料項構成的

複合資料型別,所有表都習慣性地以“_info”結尾。表用於描述有層次關係的複核結構的資料,整個Class檔案本質上就是一張表,它是由小標所示的資料項構成的:

在這裡插入圖片描述

魔數和Class檔案的版本

每個Class檔案的頭四個位元組稱為魔數,它的唯一作用就是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。很多檔案儲存標準都是使用魔數而不是副檔名來進行識別,主要是基於安全方面考慮。緊接著魔數的四個位元組儲存的是Class檔案的版本號:第五和第六是此版本號,第七和第八是主版本號,以下面的類為例:

public class TestClass {

    private int m;

    public int inc() {
        return m + 1;
    }
}

wxHexEditor開啟class檔案 在這裡插入圖片描述魔數(4個位元組cafe babe)次版本號(0000) 主版本號(0034)十進位制52表示1.8 版本

常量池

緊接著主版本號之後是常量池入口,常量池可以理解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時還是在Class檔案中第一個出現的表型別資料專案。由於常量池中常量的數量不是固定的,所以在常量池的入口需要放置一項u2型別的資料u,代表常量池容量計數值,這個容量技術值是從1開始而不是從0開始的。這樣做的目的在於滿足後面某些指向常量池的索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的含義,這種情況就可以把索引值置為0來表示。上圖中常量池入口0X0016十進位制22表示常量池中有18項常量(索引為1-21) 常量池中主要存放兩大類常量:字面量和符號引用。

字面量近似於Java的常量,如文字字串、宣告為final的常量值,而符號引用則屬於編譯原理方面的概念,包括:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符。   Java程式碼在編譯時沒有連線的步驟,而是在虛擬機器載入Class檔案的時候進行動態連線。當虛擬機器執行時,需要從常量池獲得對應的符號引用,再在類建立時或執行時解析、翻譯到具體的記憶體地址之中。   javap 檢視位元組碼,可以看到21個常量

➜  classes git:(master) ✗ javap -v com.own.learn.jdk.cls1.classLoading.TestClass
Classfile /home/wangzhenya/IdeaProjects/core_java/target/classes/com/own/learn/jdk/cls1/classLoading/TestClass.class
  Last modified 2018-9-21; size 425 bytes
  MD5 checksum 36dcb3d230e67d59659b76b6936019de
  Compiled from "TestClass.java"
public class com.own.learn.jdk.cls1.classLoading.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // com/own/learn/jdk/cls1/classLoading/TestClass.m:I
   #3 = Class              #20            // com/own/learn/jdk/cls1/classLoading/TestClass
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/own/learn/jdk/cls1/classLoading/TestClass;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               TestClass.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               com/own/learn/jdk/cls1/classLoading/TestClass
  #21 = Utf8               java/lang/Object
{
  public com.own.learn.jdk.cls1.classLoading.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/own/learn/jdk/cls1/classLoading/TestClass;

  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 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/own/learn/jdk/cls1/classLoading/TestClass;
}
SourceFile: "TestClass.java"

在JDK1.7之前有11中結構不同的表結構,在JDK1.7中為了更好的支援動態語言呼叫,又增加了3種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。不過這裡不會介紹這三種表資料結構。

這14個表的開始第一個位元組是一個u1型別的tag,用來標識是哪一種常量型別。這14種常量型別所代表的含義如下

在這裡插入圖片描述    由於Class檔案中方法、欄位等都需要引用CONSTANT_UTF8_info型常量來描述名稱,所以CONSTANT_UTF8_info型常量的最大長度也就是Java中方法和欄位名的最大長度。最大值length是65535,所以Java程式中如果定義了超過64KB英文字元的變數或方法名,將會無法編譯。 給出14種常量項的結構:

在這裡插入圖片描述

在這裡插入圖片描述

訪問標誌

在常量池結束後,緊接著的2個位元組代表訪問標誌(access_flags),這個標誌用於識別一些類或藉口層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別;如果是類的話,是否被宣告為final等; 在這裡插入圖片描述

類索引、父類索引以及介面索引集合

類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合(interfaces)是一組u2型別的資料的集合,Class檔案由這三項資料來確定這個類的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。由於Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java類的父類索引都不為0。介面索引集合就用來描述這個類實現了哪些介面,這些被實現的介面將按implements語句(如果這個類本身是一個介面,則應當是extends語句)後的介面順序從左到右排列在介面的索引集合中。

類索引、父類索引和介面索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2型別的索引值表示,它們各自指向一個型別為CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info型別的常量中的索引值可以找到定義在CONSTANT_Utf8_info型別的常量中的全限定名字串。

對於介面索引集合,入口的第一項—u2型別的資料為藉口計數器(interfaces_count),表示索引表的容量。如果該類沒有任何藉口,那麼該計數器值為0,後面的介面的索引表不再佔用任何位元組。

   #3 = Class              #20            // com/own/learn/jdk/cls1/classLoading/TestClass
   #4 = Class              #21            // java/lang/Object

欄位表集合

欄位表(field_info)用於描述介面或類中宣告的變數。欄位(field)包括了類級變數或例項級變數,但不包括在方法內部宣告的變數。java中描述一個欄位可以包含的資訊:欄位的作用域(public、private、protected修飾符)、是類級變數還是例項級變數(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主記憶體讀寫)、可否序列化(transient修飾符)、欄位資料型別(基本型別、物件、陣列)、欄位名稱。這些資訊中,各個修飾符都是布林值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而欄位叫什麼名字、欄位被定義為什麼資料型別,這些都是無法固定的,只能引用常量池中的常量來描述。 在這裡插入圖片描述 欄位修飾符放在access_flags專案中,它與類中的access_flags專案是非常類似的,都是一個u2的資料型別,其中可設定的標誌位和含義如下:

在這裡插入圖片描述跟隨access_flags標誌的是兩個索引值:name_index和descriptor_index。它們都是對常量池的引用,分別代表著欄位的簡單名稱及欄位和方法的描述符。 全限定名:”org/fenixsoft/clazz/TestClass”是類的全限定名,僅僅是把類全名中的“.”替換成了“/”而已,為了使連續的多個許可權定名之間不產生混淆,在使用時最後一般會加入一個“;”號表示全限定名結束。 簡單名稱:指沒有型別和引數修飾的方法或欄位名稱,例如方法inc()和欄位m的簡單名稱分別是“inc”和“m”。 描述符作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。根據描述符規則,基本資料型別(byte、char、double、float、int、long、short、boolean)及代表無返回值的void型別都用一個大寫字元來表示,而獨享型別則用字元L加物件的全限定名來 在這裡插入圖片描述 對於陣列型別,每一維度將使用一個前置的“[”字元來描述,如一個定義為“java.lang.String[][]”型別的二維陣列,將被記錄為:“[[Ljava/lang/String;”,一個整型陣列“int[]”將被記錄為“[I”。

用描述符來描述方法時,按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組小括號“()”之內。如方法void inc()的描述符為“()V”,方法java.lang.String toString()的描述符為“()Ljava/lang/String;”,方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] targetOffset,int targetCount,int fromIndex)的描述符為“([CII[CIII)I”。 欄位集合中不會列出來超類或者父介面中繼承而來的欄位,但可能列出原本java程式碼之中不存在的欄位,譬如在內部類中為了保持對外部類的訪問性,會自動新增指向外部類例項欄位。

方法表集合

Class檔案儲存格式中對方法的描述與對欄位的描述幾乎用了完全一致的方法,方法表的結構如同欄位表一樣,依次包括了訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表合集(attributes)。 在這裡插入圖片描述因為volatile關鍵字和transient關鍵字不能修飾方法,所以方法表的訪問標誌中沒有了ACC_VOLATILE標誌和ACC_TRANSIENT標誌。相對的,synchronized、native、strictfp和abstract關鍵字可以修飾的方法,所以方法表的訪問標誌中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT標誌,對於方法表,所有標誌位及其取值: 在這裡插入圖片描述 方法的定義可以通過訪問標誌、名稱索引、描述符索引表達清楚,但方法裡面的程式碼在哪?方法裡的Java程式碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡面,屬性表作為Class檔案格式中最具擴充套件性的一種資料專案。

屬性表集合

屬性表(attribute_info)在前面的講解之中已經出現過多次,在Class檔案、欄位表、方發表中都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。 與Class檔案中其他的資料專案要求嚴格的順序、長度和內容不同,屬性表集合的限制稍微寬鬆,不再要求各個屬性表具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java虛擬機器執行時會忽略掉它不認識的屬性。 為了能正確地解析Class檔案,預定義了9項虛擬機器實現應當能識別的屬性: 在這裡插入圖片描述

屬性表集合

屬性表(attribute_info)在前面的講解之中已經出現過多次,在Class檔案、欄位表、方法表中都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。

與Class檔案中其他的資料專案要求嚴格的順序、長度和內容不同,屬性表集合的限制稍微寬鬆,不再要求各個屬性表具有嚴格的順序,並且只要不與已有的屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java虛擬機器執行時會忽略掉它不認識的屬性。

code

Java程式方法體裡的程式碼經過Javac編譯器處理之後,最終變為位元組碼指令儲存在Code屬性內。Code屬性出現在方法表的屬性集合中,但並非所有方法都必須存在這個屬性表,譬如介面或抽象類中的抽象方法就不存在Code屬性,如果方法有Code屬性表存在,那麼它的結構如下表: 在這裡插入圖片描述

attribute_name_index是一項指向CONSTANT_Utf8_info常量表的索引,常量值固定為“Code”,它代表了該屬性的屬性名稱,attribute_length指示了屬性值的長度,由於屬性名稱索引與屬性長度一共是6個位元組,所以屬性值的長度固定為整個屬性表的長度減去6個位元組。 max_stack代表了運算元棧(Operand Stacks)的最大深度。在方法執行的任意時刻,運算元棧都不會超過這個深度。虛擬機器執行的時候需要根據這個值來分配棧幀(Frame)中的運算元棧深度。 max_locals代表了局部變量表所需的儲存空間。在這裡,max_locals的單位是Slot,Slot是虛擬機器為區域性變量表分配記憶體所使用的最小單位。對於byte,char,float,int,shot,boolean,reference和returnAddress等長度不超過32位的資料型別,每個區域性變數佔1個Slot,而double與long這兩種64位的資料型別而需要2個Slot來存放。方法引數(包括例項方法中的隱藏引數“this”),顯示異常處理器的引數(Exception Handler Parameter,即try-catch語句中catch塊所定義的異常),方法體中定義的區域性變數都需要使用區域性表來存放。另外,並不是在方法中使用了多個區域性變數,就把這些區域性變數所佔的Slot之和作為max_locals的值,原因是區域性變量表中的Slot可以重用,當代碼執行超出一個區域性變數的作用域時,這個區域性變數所在的Slot就可以被其他區域性變數所使用,編譯器會根據變數的作用域來分類Slot並分配給各個變數使用,然後計算出max_locals的大小。

code_length和code用來儲存Java源程式編譯後生成的位元組碼指令。code_length代表位元組碼長度,code是用於儲存位元組碼指令的一系列位元組流。既然名為位元組碼指令,那麼每個指令就是一個u1型別的單位元組,當虛擬機器讀取到code中的一個位元組碼時,就可相應地找出這個位元組碼代表的是什麼指令,並且可以知道這條指令後面是否需要跟隨引數,以及引數應該如何理解。 關於code_length還有一件值得注意的事情,雖然它是一個u4型別的長度值,理論上最大值可以達到2的32次方減1,但虛擬機器規範中限制了一個方法不允許超過65535條位元組碼指令,如果超過這個限制,Javac編譯器就會拒絕編譯。一般來講,只要我們寫Java程式碼時不是刻意地編寫超長的方法,就不會超過這個最大值限制。但是,在編譯複雜的JSP檔案中,可以會因為這個原因導致編譯失敗。 Code屬性是Class檔案中最重要的一個屬性,如果表一個Java程式中的資訊分為程式碼(Code,方法體裡的Java程式碼)和元資料(Metadata,包括類、欄位、方法定義及其它資訊)兩部分,那麼在整個Class檔案裡,Code屬性用於描述程式碼,其它的所有資料專案就都用於描述元資料。 在位元組碼指令之後的是這個方法的顯示異常處理表,異常表對於Code屬性表來說不是必須存在的。異常表的格式如下表: 在這裡插入圖片描述 在位元組碼指令之後的是這個方法的顯示異常處理表,異常表對於Code屬性表來說不是必須存在的。異常表的格式如下表: 在這裡插入圖片描述 異常表它包含4個欄位,這些欄位的含義為:如果位元組碼從第start_pc到end_pc行之間(不包含第end_pc)行出現了型別為catch_type或其子類的異常(catch_type為指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理。當catch_type的值為0時,代表任何的異常情況都需要轉向到handler_pc行行進行處理。異常表實際上是Java程式碼的一部分,編譯器使用異常表而不是簡單的跳轉命令來實現Java異常及finally處理機制。注:位元組碼的“行”是一種形象的描述,指的是位元組碼相對於方法體開始的偏移量,而不是Java原始碼的行號。

Exceptions屬性

這裡的Exceptions屬性是在方法表中與Code屬性平級的一項屬性,而不是Code屬性表中的異常屬性表。Exceptions屬性表的作是列舉出方法中可能丟擲的受查檢(Checked Exception),也就是在方法描述時在throws關鍵字後面列舉的異常。它的結構如下表: 在這裡插入圖片描述 此屬性表中的number_of_exceptions項表示訪求可能丟擲number_of_exceptions種受檢查異常,每一種受檢查異常使用一個exception_index_table項表示,為指向常量池中CONSTANT_Class_info型常量表的索引,代表了該受檢查異常的型別。

ConstantValue屬性

ConstantValue屬性的作用是通知虛擬機器自動為靜態變數賦值。只有被static關鍵字修飾的變數才可以使用這項屬性。在Java程式裡類類似“int x = 123“和”static int x = 123”這樣的變數定義非常常見,但虛擬機器對這兩種變數賦值的方法和時刻有所不同。對於非static型別的變數(也就是例項變數)的賦值是在例項構造器方法中進行的;對於類變數,則有兩種式可以選擇:賦值在類構造器方法中進行,或者使用ConstantValue屬性來賦值。目前Sun Javac編譯器的選擇是:如果同時使用final和static來修改一個變數,並且這個變數的資料型別是基本型別或java.lang.String的話,就生成ConstantValue屬性來進行初始化,如果這個變數沒有被final修飾,或者並非基本型別或字串,則選擇在類構造器中進行初始化。ConstantValue屬性表結構如下: 在這裡插入圖片描述 ConstantValue屬性是一個定長屬性,它的attribute_length資料項值必須為2。constantvalue_index資料項代表了常量池中一個字面常量的引用,根據欄位型別不同,字面量可以是CONSTANT_Long_info,CONSTANT_Float_info,CONSTANT_Double_info,CONSTANT_Integer_info和CONSTANT_String_info常量中的一種。