JVM學習--Class類檔案結構

sayWhat_sayHello發表於2018-12-07

一個例子(下面的表根據這個例子講解)

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

經過javac test.java生成test.class。使用WinHex檢視如下:
在這裡插入圖片描述

Class類檔案結構

Class檔案格式

u1~8代表 1 ~ 8個位元組的無符號數,_info代表表

型別 名稱 數量
u4 魔術 1
u2 次版本號 1
u2 主版本號 1
u2 常量池數量 1
cp_info constant_pool 常量池數量 - 1
u2 訪問標識 1
u2 本類標識 1
u2 父類標識 1
u2 介面數量 1
u2 介面 介面數量
u2 欄位數量 1
field_info 欄位 欄位數量
u2 方法數量 1
method_info 方法 方法數量
u2 屬性數量 1
attribute_info 屬性 屬性數量

魔術

標識這個檔案是java檔案,固定為:

CA FE BA BE

cafe babe記為咖啡寶貝。

次版本號

現在好像基本都是

00 00

主版本號

00 34

34(16進位制) = 52(10進位制)

版本和版本號對應如下:

Java 1.2 uses major version 46
Java 1.3 uses major version 47
Java 1.4 uses major version 48
Java 5 uses major version 49
Java 6 uses major version 50
Java 7 uses major version 51
Java 8 uses major version 52
Java 9 uses major version 53
Java 10 uses major version 54
Java 11 uses major version 55

常量池計數

需要注意的是計數是從1開始的。0代表的是某些指向常量池索引值的資料在特定情況下需要表達“不引用任何一個常量池專案”的意思。

00 13

0x0013 -> 19 代表常量池中有19-1=18項常量,索引為1~18.

常量池

常量池中存放兩大類常量:字面量(literal)和符合引用(Symbolic References)。
字面量接近於java語言層面的常量概念,如String,被宣告為final的常量值等;
符合引用主要包含以下三種常量:

  • 類和介面的全限定名
  • 欄位的名稱和描述符
  • 方法的名稱和描述符

當虛擬機器執行時,需要從常量池獲取對應的符號引用,再在類建立時或執行時解析並翻譯到具體的記憶體地址中。
這些表都有一個共同的特點,就是表的第一位是一個u1型別的標誌位。

Constant Type Value 描述
CONSTANT_Class 7 類或介面的符號引用
CONSTANT_Fieldref 9 欄位的符號引用
CONSTANT_Methodref 10 類中方法的符號引用
CONSTANT_InterfaceMethodref 11 介面中方法的符號引用
CONSTANT_String 8 字串型別的字面量
CONSTANT_Integer 3 整型字面量
CONSTANT_Float 4 浮點型字面量
CONSTANT_Long 5 長整型字面量
CONSTANT_Double 6 雙精度浮點型字面量
CONSTANT_NameAndType 12 欄位或方法的部分符號引用
CONSTANT_Utf8 1 utf-8編碼的字串
CONSTANT_MethodHandle 15 方法處理控制程式碼
CONSTANT_MethodType 16 方法型別
CONSTANT_InvokeDynamic 18 動態方法的呼叫點

由於每一項常量表都有自己的結構,這裡就不展開講。就講第一個

0A

0A -> 10 對照上面的表發現是類中方法的符號引用。去查對應的結構為:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

u1一個位元組的tag已經讀取了,接下來是u2兩個位元組的類索引

00 04

這個index代表指向常量池第4項,這裡不能直接得到第4項還要接著往下看。

接下來是u2兩個位元組的欄位或方法的部分符號的索引

00 0F

這個index指向常量池第15項,目前也無法得到o(╥﹏╥)o
這裡刷下花招,用javap看看。發現上面的解析並沒有錯誤。
在這裡插入圖片描述

訪問標誌

訪問標誌用於識別一些類或介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public等。

Flag Name Value 描述
ACC_PUBLIC 0x0001 標識是public型別
ACC_FINAL 0x0010 標識是final型別
ACC_SUPER 0x0020 標識允許使用invokespecial位元組碼指令,JDK1.2後預設為true
ACC_INTERFACE 0x0200 標識這是一個介面
ACC_ABSTRACT 0x0400 標識抽象型別
ACC_SYNTHETIC 0x1000 標識這個類並非由使用者程式碼生成
ACC_ANNOTATION 0x2000 標識這是一個註解
ACC_ENUM 0x4000 標識這是一個列舉

access_flags中共有16個標誌位可以使用,當前只定義了其中8個,沒有使用到的標誌位一律為0。
使用到的標識value進行 運算,例如test類只用到了public修飾,所以ACC_PUBLIC 和 ACC_SUPER為真。所以值為0x0001 | 0x0020 = 0x0021
在這裡插入圖片描述

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

類索引和父類索引都是一個u2型別的資料,介面集合(包含介面數量+介面)是一組u2型別的資料的集合,Class由這3項資料確定類的繼承關係。
類索引是類的全限定名。
父類索引是類的父類也就是extends。
介面數量+介面表明介面的個數和名稱就是implements(如果這個類是介面則是extends)。
索引指向的是常量池裡的CONSTANT_Class_info,這個索引最終會指向一個CONSTANT_Uft8_info型別的全限定名字串。
除了java.lang.Object其他所有的父索引都不為0.

如果介面數量為0,後面的介面不再佔用任何位元組。

欄位表集合

欄位表用於描述介面或類中宣告的變數。欄位包含了類級變數和例項級變數,但不包括在方法內宣告的變數。

欄位表結構:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

其中 access_flags 包含:這個很好理解,就不解釋了。

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; usable only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; never directly assigned to after object construction (JLS §17.5).
ACC_VOLATILE 0x0040 Declared volatile; cannot be cached.
ACC_TRANSIENT 0x0080 Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ENUM 0x4000 Declared as an element of an enum.

name_index和descriptor_index 都是對常量池的引用,分別代表著欄位的簡單名稱及欄位和方法的描述符。簡單名稱指的是 沒有任何修飾符的方法名或欄位名,這個類中就是inc和m。

全限定名是把.換成/,且在最後加;例如java.lang.String --> java/lang/String;

描述符用來描述欄位的資料型別、方法的引數列表和返回值。根據描述符規則,基本資料型別及代表無返回值的void型別都用一個大寫字元V來表示,而物件型別則用字元L加物件的全限定名來表示。

FieldDescriptor:
    FieldType

FieldType:
    BaseType
    ObjectType
    ArrayType	

BaseType:
    B  byte
    C  char
    D  double
    F  float
    I   int
    J  long(注意區別)
    S String
    Z  boolean
    V void返回值

ObjectType:
    L ClassName ; 例如 `Ljava/lang/Object;`

ArrayType:
    [ ComponentType   保留一個前置的括號例如:int[][]   -->  [[I

ComponentType:
    FieldType

描述方法按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組小括號裡。
例如:void inc() --> ()V 再例如java.lang.String.copyValueOf(char data[], int offset, int count) ---> ([CII)Ljava/lang/String;

在這裡插入圖片描述

選定的淺藍色就是這段內容,解讀如下:1個欄位,修飾符是2(private),欄位名是5(m),描述是6(I).連起來就是private int m

descriptor_index;後還可能有屬性表,用來描述額外的資訊。如果將欄位m的宣告改為"final static int m = 123",那就可能存在一項名為ConstantValue的屬性,值指向常量123.

欄位表不會列出從父類或介面中繼承來的欄位,但有可能列出原來java程式碼中不存在的欄位。

方法表集合

方法表結構和欄位幾乎一樣。

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

方法訪問標誌有所差異

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; must not be overridden (§5.4.5).
ACC_SYNCHRONIZED 0x0020 Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 是否是編譯器產生的橋接方法
ACC_VARARGS 0x0080 是否接受可變引數
ACC_NATIVE 0x0100 Declared native; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declared abstract; no implementation is provided.
ACC_STRICT 0x0800 Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

方法經過編譯後,方法內的程式碼存放在一個名為Code的屬性裡面,屬性表是Class檔案格式最具有擴充套件性的一種資料專案。

在這裡插入圖片描述
00 02 代表兩個方法,一個方法是編譯器新增的例項構造器,另一個是原始碼中的inc()。第一個方法的訪問標識值為00 01就是public,方法名為00 07:描述索引值為00 08對應常量為()V,屬性表計數器為00 01代表有一個屬性 再檢視00 09發現為Code,說明該屬性是方法的位元組碼描述。

如果父類方法在子類中沒有被重寫,方法表中就不會出現父類的方法資訊。但是有可能出現編譯器自動新增的方法,最典型的是類構造器和方法。

屬性表集合

屬性表限制比較寬鬆,而且可以自定義,篇幅超級無敵長,這裡就隨便拿個。。Code屬性的

Code_attribute {
    u2 attribute_name_index;  屬性名稱
    u4 attribute_length;   屬性值的長度(整個屬性表 - 6),6是attribute_name_index+ attribute_length的長度
    u2 max_stack;  運算元棧深度的最大值
    u2 max_locals;區域性變數表所需的儲存空間
    u4 code_length; 位元組碼長度
    u1 code[code_length]; 位元組碼指令的位元組流
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } 
    exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

如果位元組碼從第start_pc行到第end_pc行之間出現了型別為 catch_type或其子類的異常,則轉到第handler_pc行繼續處理。當catch_type的值為0時,代表任何的異常情況都需要轉向到 handler_pc進行處理。

參考

周志明著《深入理解java虛擬機器》

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html

相關文章