深入理解JVM類檔案格式

卡巴拉的樹發表於2017-11-20

我們知道Java最有名的宣傳口號就是:“一次編寫,到處執行(Write Once,Run Anywhere)”,而其平臺無關性則是依賴於JVM, 所有的java檔案都被編譯成位元組碼(class)檔案,而虛擬機器只需要認識位元組碼檔案就可以了。想要弄懂虛擬機器以及類載入機制,這部分內容是不可不知的。

JVM
JVM

Class檔案是一組以8位元組為基礎單位的二進位制流,所有資料無間隔的排列在Class檔案之中,多位元組資料以大端(big-endian order)的方式儲存。Class檔案以一種接近於C中結構體的虛擬碼形式儲存資料結構,並且只包含無符號數和表兩種資料結構:

  • 無符號數:u1、u2、u4、u8分別表1、2、4、8位元組的無符號數
  • : 由多個無符號數或者其他表組成的複合資料型別, Class檔案本身也是一張表。

Class表結構:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}複製程式碼

參照上面的資料結構,Class檔案由10個部分組成:
1 . 魔數
2 . Class檔案主次版本號
3 . 常量池
4 . 訪問標記
5 . 當前類名
6 . 父類名
7 . 繼承的介面名
8 . 包含的所有欄位的數量+欄位
9 . 包含的所有方法的數量+方法
10 . 包含的所有屬性的數量+屬性

下面我們依次對每個部分進行分析:

1. 魔數

魔數(Magic number)用來確定檔案型別,這裡就是檢測檔案是否是能夠被虛擬機器接受的Class檔案。很多檔案都使用魔數來確定檔案型別,而不是副檔名(因為副檔名可以任意修改)。可以參看我的深入理解程式構造(一)

Class檔案的魔數是“0xcafebabe”,咖啡寶貝?Java本身也是一種爪哇咖啡,真是挺有緣的。
這裡我也寫個小的測試程式,來看看它的二進位制碼流:

package com.shuqing28;

public class TestClass {
    private int m;
    public int inc() {
        return m+1;
    }
}複製程式碼

我們使用javac編譯成.class檔案,Windows下可以使用WinHex開啟,Linux下則可以使用hexdump開啟二進位制,命令如下:

$ hexdump -C TestClass.class 
00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|
00000010  00 03 00 13 07 00 14 07  00 15 01 00 01 6d 01 00  |.............m..|
00000020  01 49 01 00 06 3c 69 6e  69 74 3e 01 00 03 28 29  |.I...<init>...()|
00000030  56 01 00 04 43 6f 64 65  01 00 0f 4c 69 6e 65 4e  |V...Code...LineN|
00000040  75 6d 62 65 72 54 61 62  6c 65 01 00 12 4c 6f 63  |umberTable...Loc|
00000050  61 6c 56 61 72 69 61 62  6c 65 54 61 62 6c 65 01  |alVariableTable.|
00000060  00 04 74 68 69 73 01 00  19 4c 63 6f 6d 2f 73 68  |..this...Lcom/sh|
00000070  75 71 69 6e 67 32 38 2f  54 65 73 74 43 6c 61 73  |uqing28/TestClas|
00000080  73 3b 01 00 03 69 6e 63  01 00 03 28 29 49 01 00  |s;...inc...()I..|
00000090  0a 53 6f 75 72 63 65 46  69 6c 65 01 00 0e 54 65  |.SourceFile...Te|
000000a0  73 74 43 6c 61 73 73 2e  6a 61 76 61 0c 00 07 00  |stClass.java....|
000000b0  08 0c 00 05 00 06 01 00  17 63 6f 6d 2f 73 68 75  |.........com/shu|
000000c0  71 69 6e 67 32 38 2f 54  65 73 74 43 6c 61 73 73  |qing28/TestClass|
000000d0  01 00 10 6a 61 76 61 2f  6c 61 6e 67 2f 4f 62 6a  |...java/lang/Obj|
000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|
000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|
00000100  09 00 00 00 2f 00 01 00  01 00 00 00 05 2a b7 00  |..../........*..|
00000110  01 b1 00 00 00 02 00 0a  00 00 00 06 00 01 00 00  |................|
00000120  00 03 00 0b 00 00 00 0c  00 01 00 00 00 05 00 0c  |................|
00000130  00 0d 00 00 00 01 00 0e  00 0f 00 01 00 09 00 00  |................|
00000140  00 31 00 02 00 01 00 00  00 07 2a b4 00 02 04 60  |.1........*....`|
00000150  ac 00 00 00 02 00 0a 00  00 00 06 00 01 00 00 00  |................|
00000160  06 00 0b 00 00 00 0c 00  01 00 00 00 07 00 0c 00  |................|
00000170  0d 00 00 00 01 00 10 00  00 00 02 00 11           |.............|
0000017d複製程式碼

看第一行的前4個位元組的十六進位制就是0xcafebabe,所以檔案型別確實為.class檔案。

2. 版本號

第5和第6位元組是次版本號(Minor Version),第7和第8位元組是主版本號(Major Version)。這裡看出我們的主版本號是0x0034,也就是52,下面是JDK與其對應的版本號關係:

JDK 1.8 = 52
JDK 1.7 = 51
JDK 1.6 =50
JDK 1.5 = 49
JDK 1.4 = 48
JDK 1.3 = 47
JDK 1.2 = 46
JDK 1.1 = 45

可以看出我使用的是Java8編譯的程式碼。

3. 常量池

我們繼續看二進位制檔案的第一行:

00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|複製程式碼

在主版本號0x0034後的是0x0016,這個值表示常量池的容量。常量池可以理解為Class檔案的資源倉庫,常量池中包含的資料結構是這樣的:

cp_info {
    u1 tag;
    u1 info[];
}複製程式碼

常量池中的每個專案都包含一個tag開頭的cp_info物件,代表著常量型別,info則根據不同的型別各有各的結構。目前一共有14種常量型別:

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
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

上面的0x0016翻譯成十進位制是22,那麼常量池中有21個常量,因為常量池中索引是從1開始計數的,所以常量索引範圍是1~21。

00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|複製程式碼

接下看常量池的第一個常量, tag是0x0a, 查上面的常量表就是CONSTANT_Methodref,表示接下來定義的是一個方法,知道型別後,我們可以查一下CONSTANT_Methodref的結構,這裡可以參考Oracle的官方文件The class File Format,

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}複製程式碼

由於.class檔案是無間隔的二進位制檔案,所以接著讀:

  • tag: 0x0a,上面已經說了指代CONSTANT_Methodref常量
  • class_index:指向常量池中CONSTANT_Class_info型別的常量,代表上面方法的名稱
  • name_and_type_index : 指向常量池中CONSTANT_NameAndType_info常量,是對方法的描述

因為class_index佔兩個位元組,所以緊接著讀到了0x0004,也就是4,指向常量池中的第4個常量,name_and_type_index是0x0012,指向第18個常量。後面會分析到第4和第18個常量。

繼續往下讀,到第一行的最末了,是個0x09,指示的是CONSTANT_Fieldref,表示接下來是對一個域的定義, 查官方文件,格式為:

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}複製程式碼

結構和CONSTANT_Methodref_info一樣,這時候讀到了第二行:

00000010  00 03 00 13 07 00 14 07  00 15 01 00 01 6d 01 00  |.............m..|複製程式碼

class_index為0x0003,指向第3個常量,name_and_type_index為0x0013指向第13個常量。這時候繼續往後讀,終於讀到第3個常量了。此時tag是0x07,查表可得為CONSTANT_Class型別,此型別的常量代表一個類或者介面的符號引用,CONSTANT_Class的結構:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}複製程式碼

tag是7, name_index是0x0014,十進位制就是20,指向第20個常量,這樣我們已經讀了很多個位元組了。但是這樣解析下去很累,還好java自帶的javap工具可以幫我們分析出位元組碼的內容。
執行下面語句:

javap -verbose TestClass.class複製程式碼

我們可以得到:

Last modified Nov 14, 2017; size 381 bytes
  MD5 checksum 102d643185c4823ef103931ff3e34462
  Compiled from "TestClass.java"
public class com.shuqing28.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/shuqing28/TestClass.m:I
   #3 = Class              #20            // com/shuqing28/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/shuqing28/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/shuqing28/TestClass
  #21 = Utf8               java/lang/Object
{
  public com.shuqing28.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
...//省略複製程式碼

這裡我們可以看到Constant pool欄位,後面依次列出了21個常量,可以看出第一個是Methodref型的常量,class_index指向第4個常量,第4個常量呢是CONSTANT_Class型別,name_index又指向第20個常量,可知是一個CONSTANT_Utf8型別的常量,前面沒說到CONSTANT_Utf8,下面是它的結構:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}複製程式碼

第一位tag為1,length指示字元陣列的長度,bytes[length]是使用UTF-8縮略編碼表示的字串,這裡解析出來是com/shuqing28/TestClass,即類的全限定名。

繼續回到第一個Methodref常量,它的name_and_type_index值是18, 繼續找到第18個常量,是CONSTANT_NameAndType_info型別,代表的是一個方法的資訊:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}複製程式碼

name_index指向了常量7, 即#7 = Utf8 <init>, 是一個CONSTANT_Utf8_info型別,值為,這個是方法的名稱,descriptor_index指向了常量8,即#8 = Utf8 ()V,是方法的描述,下文會說這個表示式是什麼意思。
這樣我們就可以一一把這21個常量分析清楚了。

其實Class檔案就是在一開始列出了一堆常量,後面的各種描述都是各種index,指向前面常量池中的各種常量,來描述整個類的定義。就像有一本字典,我們使用字典中的字來造我們的句子,只不過Class檔案中造句是有嚴格格式規定的,下面的內容基本都按照固定格式,無間隔的描述一個類的內容。

4. 訪問標誌

常量池結束後,緊接著的兩個位元組代表訪問標誌(access_flags),這個標誌用於識別一些類或者介面的訪問資訊,包括這個Class是類還是介面,是否是public的,是否是abstract,是否是final的。
訪問標記含義如下表:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

access_flags中一共有16個標誌位可用,當前只定義了8個,別的都為0,TestClass是public型別的,且使用JDK1.2以後的編譯器進行編譯的(使用JDK1.2以後的編譯器編譯,這個值都為真),別的標誌都為假。所以access_flags的值應為:0x0001|0x0020 = 0x0021。我們找到剛才常量池最後一行的地方:

000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|複製程式碼

65 63 74分別對應ect,緊接著是0x0021,與我們的分析結果一致。

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

引用文章開頭的ClassFile的資料結構,這三項定義為:

    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];複製程式碼

類索引和父類索引都是u2型別的資料,而介面索引首先給出了介面的數量,然後才是一個包含介面的陣列。這三個值揭示了一個類的繼承關係。

000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|複製程式碼

接著前面的0x0021看,類索引為0x0003,指示常量池第3個常量,查上文可得#3 = Class #20 // com/shuqing28/TestClass,第3個常量又指向第20個常量,而第20個常量是一個CONSTANT_Utf8變數,其值為com/shuqing28/TestClass,表示類的全限定名字串。
接下來的是0x0004是父類索引,指向常量池中第4個常量,即#4 = Class #21 // java/lang/Object, 又指向第21個變數,即java/lang/Object,我們知道Object是所有類的父類。
接下來的是0x0000,可見TestClass沒有實現任何介面。

6.欄位表集合

欄位表用於描述介面或者類中宣告的變數。欄位包括類級別的變數以及例項級的變數,但是不包括方法內的區域性變數。一個Java欄位可以包括以下資訊:欄位的作用域、是例項變數還是類變數、是否是final、併發可見性(volatile),是否可以被序列化(transient)、欄位資料型別。下面是欄位表具體結構:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}複製程式碼

再看access_flags可以取以下值:

標誌名稱 標誌值 含義
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.

一般來說,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個標誌最多隻能存在一個,其它標誌都按照Java語言本身的性質來。

在access_flags標誌的後面是兩項索引值name_index,descriptor_index,兩個都是指向常量池的索引,分別代表欄位的簡單名稱以及欄位和方法的描述符。

這裡我們梳理下簡單名稱、描述符以及全限定名這三個詞對應的概念:
全限定名:前面提到的com/shuqing28/TestClass就是全限定名,它把java程式碼中所有的"."替換成了"/",一般使用";"結尾。
簡單名稱:不帶型別和修飾的方法或者欄位名,上文中的程式碼裡就是"inc"和"m"
至於方法描述符,描述的是資料型別、方法的引數列表和返回值。我們知道在C++中過載函式時函式實際上是換了名字的,包含了函式的引數,例如add(int x, int y),在編譯後可能是Add_Int_Int, 但是在Java中我們把基本資料型別都用一個大寫字元來表示,而物件類則是使用L+物件的全限定名來表示。

描述符標識字元含義:

標識字元 含義
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L Object, 例如 Ljava/lang/Object

對於陣列,前面加[就行,如java.lang.String[][],表達為[[java/lang/String, int[] 就被記錄為[I
用描述符描述方法時,按照引數列表,返回值的順序描述,引數列表還需要放在括號內。比如前文提及的"() V" 就表示一個引數為空,返回值為void的方法,即程式碼中的void inc()方法。

舉個複雜點的, int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex),其描述符為([CII[CIII) I

繼續分析我們前文中提及的程式的二進位制程式碼:

000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|
000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|複製程式碼

上一小節我們分析到第一行的0x0000了,接下來的是0x01,這個值其實代表了欄位表的個數,我們的程式碼裡只包含一個欄位。接下來的是0x0002,這個欄位是access_flags標誌,查詢後可知為ACC_PRIVATE,再接下來是0x0005, 從常量表清單上可以查到是#5 = Utf8 m, 再接著是descriptor_index, 其值為0x0006,查一下常量池為#6 = Utf8 I,可知這一句為private int m;

一般來說,在decriptor_index後,還有個屬性集合用於儲存一些額外資訊,而0x0000代表沒有屬性欄位。
如果把m欄位宣告為private static int m = 123; 則可能多一個ConstantValue屬性,指向常量值123。

7.方法表集合

方法表集合和欄位表集合非常相似,結構也是:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}複製程式碼

只不過在訪問標誌和屬性表集合的可選項有所不同。例如access_flags有以下可選值:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declaredprivate; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declaredprotected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declaredstatic.
ACC_FINAL 0x0010 Declaredfinal; must not be overridden
ACC_SYNCHRONIZED 0x0020 Declaredsynchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declarednative; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declaredabstract; no implementation is provided.
ACC_STRICT 0x0800 Declaredstrictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

可以看出,方法裡增加了像ACC_SYNCHRONIZED,ACC_NATIVE,ACC_STRICT, ACC_ABSTRACT, 分別對應著synchronizednativestrictfpabstract這些只能修飾方法的關鍵字。

現在我們就可以繼續分析我們程式的二進位制程式碼了。

000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|
00000100  09 00 00 00 2f 00 01 00  01 00 00 00 05 2a b7 00  |..../........*..|複製程式碼

上一小節我們剛剛分析到000000f0行的0x0000,接下來的是0x0002,代表有兩個方法,接下來的幾個位元組是

  • 0x0001:訪問標記是ACC_PUBLIC
  • 0x0007:名稱索引指向第7個常量:
  • 0x0008:描述符索引指向第8個常量:()V
  • 0x0001:屬性有一個
  • 0x0009:屬性指向第9個常量,Code

我們正好有疑問,方法定義有了,方法體在哪呢,答案就是上面分析的最後一個Code。下一節就說說屬性表集合的各種可能。

8.屬性表集合

屬性表(attribute_info)在前面已經多次提及,Class檔案、欄位表、方法表中都可以攜帶自己的屬性表集合,用於描述某些場景轉有的資訊。
屬性表並沒有嚴格限制順序,只要不與已有屬性名重複,任何人實現的編譯器都可以新增自己定義的屬性資訊,以下是一些預定義的屬性:

屬性名稱 使用位置 含義
SourceFile ClassFile 記錄原始檔的名稱
InnerClasses ClassFile 內部類列表
EnclosingMethod ClassFile 內部類才有這個屬性,用於標識這個類所在的外圍方法
SourceDebugExtension ClassFile 用於儲存額外的除錯資訊,JDK1.6中新增
BootstrapMethods ClassFile 用於儲存invokeddynamic指令引用的引導方法限定符,JDK1.7中新增
ConstantValue field_info final關鍵字定義的常量值
Code method_info Java程式碼編譯成的位元組碼指令
Exceptions method_info 方法丟擲的異常
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations method_info 指明哪些引數是執行時可見的,哪些是執行時不可見的,JDK1.5中新增
AnnotationDefault method_info 記錄註解類元素的預設值,JDK1.5中新增的
MethodParameters method_info 記錄方法的引數資訊,比如它們的名字,訪問級別,JDK1.8新增
Synthetic ClassFile, field_info, method_info 表示方法或欄位是編譯器自動生成的
Deprecated ClassFile, field_info, method_info 被宣告為deprecated的欄位
Signature ClassFile, field_info, method_info 用於支援泛型情況下的方法簽名,在Java語言中,如果任何類、介面、初始化方法或者成員的泛型簽名包含了型別變數或者引數化型別,則Signature屬性會為它記錄泛型簽名資訊。由於Java的泛型採用擦除法實現,在為了避免型別資訊被擦除後導致簽名混亂,需要這個屬性記錄泛型中的相關資訊。JDK1.5中新增
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations ClassFile, field_info, method_info 為動態註解提供支援,指明哪些是註解是執行時可見的,哪些是執行時不可見的,JDK1.5中新增
LineNumberTable Code Java原始碼的行號與位元組碼指令的對應關係
LocalVariableTable Code 方法的區域性變數描述
LocalVariableTypeTable Code 使用特徵簽名代替描述符,是為了引入泛型語法之後能描述泛型引數化型別而新增,JDK1.5中新增
StackMapTable Code 供新的型別檢查驗證器(Type Checker)檢查和處理目標方法的區域性變數和操作棧所需要的型別是否匹配,JDK1.6新增
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations ClassFile, field_info, method_info, Code 記錄執行時型別上註解的可見性,也包括執行時型別引數的註解的可見性

下面具體說一說一些比較重要的屬性:

Code屬性

首先來看Code屬性的結構:

Code_attribute {
    u2 attribute_name_index;
    u4 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];
}複製程式碼
  • attribute_name_index: 佔兩個位元組,指向CONSTANT_Utf8_info常量,表示屬性名,這裡固定是"Code"
  • attribute_length:屬性值的長度,由於attribute_name_index和attribute_length佔6個位元組,所以attribute_length為屬性表長度減6
  • max_statck: 運算元棧深度的最大值,在方法執行時,運算元棧不能超過這個值
  • max_locals: 區域性變數所需的儲存空間。max_locals的單位是Slot,Slot是虛擬機器為區域性變數分配的最小單位,對於byte,char,float,int,short,boolean和returnAddress等長度不超過32位的資料型別,都只佔一個slot,而double和long 這種64為的資料都是需要佔用兩個slot。方法引數(包括隱藏的this)、異常處理器的引數、方法體定義的區域性變數都需要區域性變數表來存放。但是max_locals並不是所有區域性變數所佔的slot之和,因為slot可以重用,當一個變數超出作用域了,該slot又會給別的區域性變數使用,編譯器會根據作用域計算max_locals。
  • code_length: 編譯器編譯後的位元組碼長度
  • code: 用於儲存位元組碼指令的一系列位元組流,每個指令是一個u1型別的單位元組,當虛擬機器讀到該位元組時,就可以知道是什麼指令,知道是什麼指令,就知道指令需要什麼運算元,繼續讀就可以了,這裡類似於彙編,u1的取值範圍是0~255,可以表達256條指令。Java虛擬機器規範中定義了約200條指令,參看Instructions。關於這部分內容以後再寫部落格介紹了。
  • exception_table_length:異常表的長度
  • exception_table: 異常表對於Code來說並不是必須存在的,所以上述長度也可以為0,異常表有4個屬性,代表著如果在start_pc到end_pc之間出現catch_type型別的異常,就跳轉到handler_pc所指向的行處理。

Exceptions屬性

Exceptions屬性在方法表中與Code屬性平級,注意和上面Code中的異常表不同,Exceptions屬性的作用是列出方法可能丟擲的異常,Exceptions屬性表的結構:

Exceptions_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}複製程式碼
  • number_of_exceptions: 可能丟擲的異常種類的個數
  • exception_index_table:指向常量池中CONSTANT_Class_info的索引

LineNumberTable屬性

LineNumber用來記錄Java原始碼與位元組碼行號之間的對應關係,我們在編譯程式碼時也可以使用-g: none-g: line來取消生成這個屬性,不過在除錯程式碼時就看不到行號了,也無法打斷點。

LineNumberTable的資料結構如下:

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number;    
    } line_number_table[line_number_table_length];
}複製程式碼

我們主要看line_number_table,start_pc是位元組碼行號,line_number是Java原始碼行號。

LocalVariableTable屬性

LocalVariableTable屬性用於描述棧幀中區域性變數表中的變數與Java原始碼中定義的變數之間的關係,我們在編譯程式碼時也可以使用-g: none-g: vars來取消生成這個屬性,但是如果取消的話,IDE會用arg0,arg1這樣的引數來取代原有的引數名,導致除錯時不清晰。
LocalVariableTable的資料結構如下:

LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}複製程式碼

主要介紹local_variable_table:

  • start_pc和length: 分別代表了這個區域性變數的生命週期開始的位元組碼偏移量以及作用範圍覆蓋的長度
  • name_index和descriptor_index:分別指向代表區域性變數名稱和區域性變數描述符的常量
  • index: 是該區域性變數在區域性變數表中的slot位置,如果變數時double 或者long型別的,佔用的slot為index和index+1兩個。

ConstantValue屬性

ConstantValue是一個定長屬性,用來通知虛擬機器為靜態變數賦值,如果同時定義了int x=3;static int y=3;則虛擬機器為x,y賦值的時機不同,對於x,是在例項構造器<init>中進行的,而static型別的變數,則會在類構造器<clinit>方法中或者使用ConstantValue屬性。
目前javac編譯器的規則是,如果同時有final和static修飾,則是使用ConstantValue屬性,只有static時,並且變數型別是基本型別或者String時,就會在<clinit>中進行初始化。

InnerClasses屬性

如果類中定義了內部類,則會使用InnerClasses屬性來記錄內部類和宿主的關係。
InnerClasses的資料結構如下:

InnerClasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;   //記錄有多少個內部類
    {   u2 inner_class_info_index;
        u2 outer_class_info_index;
        u2 inner_name_index;
        u2 inner_class_access_flags;
    } classes[number_of_classes];
}複製程式碼

還是隻看classes欄位,inner_class_info_index指向內部類的符號引用,outer_class_info_index指向宿主類的符號引用,inner_name_index指向內部類的名稱,如果是匿名內部類,則為0,inner_class_access_flags是內部類的訪問標誌,見下表:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 Marked or implicitly public in source.
ACC_PRIVATE 0x0002 Marked private in source.
ACC_PROTECTED 0x0004 Marked protected in source.
ACC_STATIC 0x0008 Marked or implicitly static in source.
ACC_FINAL 0x0010 Marked final in source.
ACC_INTERFACE 0x0200 Was an interface in source.
ACC_ABSTRACT 0x0400 Marked or implicitly abstract in source.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

還有其它的一些屬性,如果想了解,可以看一下參考資料。

參考資料:

  1. Java Virtual Machine Specification
  2. Java虛擬機器:JVM高階特性與最佳實踐

相關文章