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來x修飾一個變量,並且這個變量的數據類型是基本類型或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常量中的一種。

StackMapTable屬性

是一個複雜的變長屬性,位於Code屬性的屬性表,這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用,目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器。
StackMapTable屬性中包含零至多個棧映射棧(Stack Map Frames),每個棧映射幀都顯示或隱式的代表了一個字節碼偏移量,用於表示該執行到該字節碼時局部變量表和操作數棧的驗證類型。類型檢查驗證器會通過檢查目標方法的局部變量和操作數棧所需要的類型來確定一段字節碼指令是否符合邏輯約束。一個方法的Code屬性最多只能有一個StackMapTable屬性,否則將拋出ClassFormatError異常。 StackMapTable屬性的結構見下表。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章