深入理解JVM类文件格式
我們知道Java最有名的宣傳口號(hào)就是:“一次編寫,到處運(yùn)行(Write Once,Run Anywhere)”,而其平臺(tái)無(wú)關(guān)性則是依賴于JVM, 所有的java文件都被編譯成字節(jié)碼(class)文件,而虛擬機(jī)只需要認(rèn)識(shí)字節(jié)碼文件就可以了。想要弄懂虛擬機(jī)以及類加載機(jī)制,這部分內(nèi)容是不可不知的。
Class文件是一組以8字節(jié)為基礎(chǔ)單位的二進(jìn)制流,所有數(shù)據(jù)無(wú)間隔的排列在Class文件之中,多字節(jié)數(shù)據(jù)以大端(big-endian order)的方式存儲(chǔ)。Class文件以一種接近于C中結(jié)構(gòu)體的偽代碼形式存儲(chǔ)數(shù)據(jù)結(jié)構(gòu),并且只包含無(wú)符號(hào)數(shù)和表兩種數(shù)據(jù)結(jié)構(gòu):
- 無(wú)符號(hào)數(shù):u1、u2、u4、u8分別表1、2、4、8字節(jié)的無(wú)符號(hào)數(shù)
- 表: 由多個(gè)無(wú)符號(hào)數(shù)或者其他表組成的復(fù)合數(shù)據(jù)類型, Class文件本身也是一張表。
Class表結(jié)構(gòu):
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]; }復(fù)制代碼參照上面的數(shù)據(jù)結(jié)構(gòu),Class文件由10個(gè)部分組成:
1 . 魔數(shù)
2 . Class文件主次版本號(hào)
3 . 常量池
4 . 訪問(wèn)標(biāo)記
5 . 當(dāng)前類名
6 . 父類名
7 . 繼承的接口名
8 . 包含的所有字段的數(shù)量+字段
9 . 包含的所有方法的數(shù)量+方法
10 . 包含的所有屬性的數(shù)量+屬性
下面我們依次對(duì)每個(gè)部分進(jìn)行分析:
1. 魔數(shù)
魔數(shù)(Magic number)用來(lái)確定文件類型,這里就是檢測(cè)文件是否是能夠被虛擬機(jī)接受的Class文件。很多文件都使用魔數(shù)來(lái)確定文件類型,而不是擴(kuò)展名(因?yàn)閿U(kuò)展名可以任意修改)。可以參看我的深入理解程序構(gòu)造(一)。
Class文件的魔數(shù)是“0xcafebabe”,咖啡寶貝?Java本身也是一種爪哇咖啡,真是挺有緣的。
這里我也寫個(gè)小的測(cè)試程序,來(lái)看看它的二進(jìn)制碼流:
我們使用javac編譯成.class文件,Windows下可以使用WinHex打開(kāi),Linux下則可以使用hexdump打開(kāi)二進(jìn)制,命令如下:
$ 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復(fù)制代碼看第一行的前4個(gè)字節(jié)的十六進(jìn)制就是0xcafebabe,所以文件類型確實(shí)為.class文件。
2. 版本號(hào)
第5和第6字節(jié)是次版本號(hào)(Minor Version),第7和第8字節(jié)是主版本號(hào)(Major Version)。這里看出我們的主版本號(hào)是0x0034,也就是52,下面是JDK與其對(duì)應(yīng)的版本號(hào)關(guān)系:
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. 常量池
我們繼續(xù)看二進(jìn)制文件的第一行:
00000000 ca fe ba be 00 00 00 34 00 16 0a 00 04 00 12 09 |.......4........|復(fù)制代碼在主版本號(hào)0x0034后的是0x0016,這個(gè)值表示常量池的容量。常量池可以理解為Class文件的資源倉(cāng)庫(kù),常量池中包含的數(shù)據(jù)結(jié)構(gòu)是這樣的:
cp_info {u1 tag;u1 info[]; }復(fù)制代碼常量池中的每個(gè)項(xiàng)目都包含一個(gè)tag開(kāi)頭的cp_info對(duì)象,代表著常量類型,info則根據(jù)不同的類型各有各的結(jié)構(gòu)。目前一共有14種常量類型:
| 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翻譯成十進(jìn)制是22,那么常量池中有21個(gè)常量,因?yàn)槌A砍刂兴饕菑?開(kāi)始計(jì)數(shù)的,所以常量索引范圍是1~21。
00000000 ca fe ba be 00 00 00 34 00 16 0a 00 04 00 12 09 |.......4........|復(fù)制代碼接下看常量池的第一個(gè)常量, tag是0x0a, 查上面的常量表就是CONSTANT_Methodref,表示接下來(lái)定義的是一個(gè)方法,知道類型后,我們可以查一下CONSTANT_Methodref的結(jié)構(gòu),這里可以參考Oracle的官方文檔The class File Format,
CONSTANT_Methodref_info {u1 tag;u2 class_index;u2 name_and_type_index; }復(fù)制代碼由于.class文件是無(wú)間隔的二進(jìn)制文件,所以接著讀:
- tag: 0x0a,上面已經(jīng)說(shuō)了指代CONSTANT_Methodref常量
- class_index:指向常量池中CONSTANT_Class_info類型的常量,代表上面方法的名稱
- name_and_type_index : 指向常量池中CONSTANT_NameAndType_info常量,是對(duì)方法的描述
因?yàn)閏lass_index占兩個(gè)字節(jié),所以緊接著讀到了0x0004,也就是4,指向常量池中的第4個(gè)常量,name_and_type_index是0x0012,指向第18個(gè)常量。后面會(huì)分析到第4和第18個(gè)常量。
繼續(xù)往下讀,到第一行的最末了,是個(gè)0x09,指示的是CONSTANT_Fieldref,表示接下來(lái)是對(duì)一個(gè)域的定義, 查官方文檔,格式為:
CONSTANT_Fieldref_info {u1 tag;u2 class_index;u2 name_and_type_index; }復(fù)制代碼結(jié)構(gòu)和CONSTANT_Methodref_info一樣,這時(shí)候讀到了第二行:
00000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 |.............m..|復(fù)制代碼class_index為0x0003,指向第3個(gè)常量,name_and_type_index為0x0013指向第13個(gè)常量。這時(shí)候繼續(xù)往后讀,終于讀到第3個(gè)常量了。此時(shí)tag是0x07,查表可得為CONSTANT_Class類型,此類型的常量代表一個(gè)類或者接口的符號(hào)引用,CONSTANT_Class的結(jié)構(gòu):
CONSTANT_Class_info {u1 tag;u2 name_index; }復(fù)制代碼tag是7, name_index是0x0014,十進(jìn)制就是20,指向第20個(gè)常量,這樣我們已經(jīng)讀了很多個(gè)字節(jié)了。但是這樣解析下去很累,還好java自帶的javap工具可以幫我們分析出字節(jié)碼的內(nèi)容。
執(zhí)行下面語(yǔ)句:
我們可以得到:
Last modified Nov 14, 2017; size 381 bytesMD5 checksum 102d643185c4823ef103931ff3e34462Compiled from "TestClass.java" public class com.shuqing28.TestClassminor version: 0major version: 52flags: 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: ()Vflags: ACC_PUBLIC ...//省略復(fù)制代碼這里我們可以看到Constant pool字段,后面依次列出了21個(gè)常量,可以看出第一個(gè)是Methodref型的常量,class_index指向第4個(gè)常量,第4個(gè)常量呢是CONSTANT_Class類型,name_index又指向第20個(gè)常量,可知是一個(gè)CONSTANT_Utf8類型的常量,前面沒(méi)說(shuō)到CONSTANT_Utf8,下面是它的結(jié)構(gòu):
CONSTANT_Utf8_info {u1 tag;u2 length;u1 bytes[length]; }復(fù)制代碼第一位tag為1,length指示字符數(shù)組的長(zhǎng)度,bytes[length]是使用UTF-8縮略編碼表示的字符串,這里解析出來(lái)是com/shuqing28/TestClass,即類的全限定名。
繼續(xù)回到第一個(gè)Methodref常量,它的name_and_type_index值是18, 繼續(xù)找到第18個(gè)常量,是CONSTANT_NameAndType_info類型,代表的是一個(gè)方法的信息:
CONSTANT_NameAndType_info {u1 tag;u2 name_index;u2 descriptor_index; }復(fù)制代碼name_index指向了常量7, 即#7 = Utf8 <init>, 是一個(gè)CONSTANT_Utf8_info類型,值為,這個(gè)是方法的名稱,descriptor_index指向了常量8,即#8 = Utf8 ()V,是方法的描述,下文會(huì)說(shuō)這個(gè)表達(dá)式是什么意思。
這樣我們就可以一一把這21個(gè)常量分析清楚了。
其實(shí)Class文件就是在一開(kāi)始列出了一堆常量,后面的各種描述都是各種index,指向前面常量池中的各種常量,來(lái)描述整個(gè)類的定義。就像有一本字典,我們使用字典中的字來(lái)造我們的句子,只不過(guò)Class文件中造句是有嚴(yán)格格式規(guī)定的,下面的內(nèi)容基本都按照固定格式,無(wú)間隔的描述一個(gè)類的內(nèi)容。
4. 訪問(wèn)標(biāo)志
常量池結(jié)束后,緊接著的兩個(gè)字節(jié)代表訪問(wèn)標(biāo)志(access_flags),這個(gè)標(biāo)志用于識(shí)別一些類或者接口的訪問(wèn)信息,包括這個(gè)Class是類還是接口,是否是public的,是否是abstract,是否是final的。
訪問(wèn)標(biāo)記含義如下表:
| 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個(gè)標(biāo)志位可用,當(dāng)前只定義了8個(gè),別的都為0,TestClass是public類型的,且使用JDK1.2以后的編譯器進(jìn)行編譯的(使用JDK1.2以后的編譯器編譯,這個(gè)值都為真),別的標(biāo)志都為假。所以access_flags的值應(yīng)為:0x0001|0x0020 = 0x0021。我們找到剛才常量池最后一行的地方:
000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|復(fù)制代碼65 63 74分別對(duì)應(yīng)ect,緊接著是0x0021,與我們的分析結(jié)果一致。
5.類索引、父類索引與接口索引集合
引用文章開(kāi)頭的ClassFile的數(shù)據(jù)結(jié)構(gòu),這三項(xiàng)定義為:
u2 this_class;u2 super_class;u2 interfaces_count;u2 interfaces[interfaces_count];復(fù)制代碼類索引和父類索引都是u2類型的數(shù)據(jù),而接口索引首先給出了接口的數(shù)量,然后才是一個(gè)包含接口的數(shù)組。這三個(gè)值揭示了一個(gè)類的繼承關(guān)系。
000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|復(fù)制代碼接著前面的0x0021看,類索引為0x0003,指示常量池第3個(gè)常量,查上文可得#3 = Class #20 // com/shuqing28/TestClass,第3個(gè)常量又指向第20個(gè)常量,而第20個(gè)常量是一個(gè)CONSTANT_Utf8變量,其值為com/shuqing28/TestClass,表示類的全限定名字符串。
接下來(lái)的是0x0004是父類索引,指向常量池中第4個(gè)常量,即#4 = Class #21 // java/lang/Object, 又指向第21個(gè)變量,即java/lang/Object,我們知道Object是所有類的父類。
接下來(lái)的是0x0000,可見(jiàn)TestClass沒(méi)有實(shí)現(xiàn)任何接口。
6.字段表集合
字段表用于描述接口或者類中聲明的變量。字段包括類級(jí)別的變量以及實(shí)例級(jí)的變量,但是不包括方法內(nèi)的局部變量。一個(gè)Java字段可以包括以下信息:字段的作用域、是實(shí)例變量還是類變量、是否是final、并發(fā)可見(jiàn)性(volatile),是否可以被序列化(transient)、字段數(shù)據(jù)類型。下面是字段表具體結(jié)構(gòu):
field_info {u2 access_flags;u2 name_index;u2 descriptor_index;u2 attributes_count;attribute_info attributes[attributes_count]; }復(fù)制代碼再看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. |
一般來(lái)說(shuō),ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個(gè)標(biāo)志最多只能存在一個(gè),其它標(biāo)志都按照J(rèn)ava語(yǔ)言本身的性質(zhì)來(lái)。
在access_flags標(biāo)志的后面是兩項(xiàng)索引值name_index,descriptor_index,兩個(gè)都是指向常量池的索引,分別代表字段的簡(jiǎn)單名稱以及字段和方法的描述符。
這里我們梳理下簡(jiǎn)單名稱、描述符以及全限定名這三個(gè)詞對(duì)應(yīng)的概念:
全限定名:前面提到的com/shuqing28/TestClass就是全限定名,它把java代碼中所有的"."替換成了"/",一般使用";"結(jié)尾。
簡(jiǎn)單名稱:不帶類型和修飾的方法或者字段名,上文中的代碼里就是"inc"和"m"
至于方法描述符,描述的是數(shù)據(jù)類型、方法的參數(shù)列表和返回值。我們知道在C++中重載函數(shù)時(shí)函數(shù)實(shí)際上是換了名字的,包含了函數(shù)的參數(shù),例如add(int x, int y),在編譯后可能是Add_Int_Int, 但是在Java中我們把基本數(shù)據(jù)類型都用一個(gè)大寫字符來(lái)表示,而對(duì)象類則是使用L+對(duì)象的全限定名來(lái)表示。
描述符標(biāo)識(shí)字符含義:
| B | byte |
| C | char |
| D | double |
| F | float |
| I | int |
| J | long |
| S | short |
| Z | boolean |
| V | void |
| L | Object, 例如 Ljava/lang/Object |
對(duì)于數(shù)組,前面加[就行,如java.lang.String[][],表達(dá)為[[java/lang/String, int[] 就被記錄為[I。
用描述符描述方法時(shí),按照參數(shù)列表,返回值的順序描述,參數(shù)列表還需要放在括號(hào)內(nèi)。比如前文提及的"() V" 就表示一個(gè)參數(shù)為空,返回值為void的方法,即代碼中的void inc()方法。
舉個(gè)復(fù)雜點(diǎn)的, int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex),其描述符為([CII[CIII) I。
繼續(xù)分析我們前文中提及的程序的二進(jìn)制代碼:
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 |................|復(fù)制代碼上一小節(jié)我們分析到第一行的0x0000了,接下來(lái)的是0x01,這個(gè)值其實(shí)代表了字段表的個(gè)數(shù),我們的代碼里只包含一個(gè)字段。接下來(lái)的是0x0002,這個(gè)字段是access_flags標(biāo)志,查詢后可知為ACC_PRIVATE,再接下來(lái)是0x0005, 從常量表清單上可以查到是#5 = Utf8 m, 再接著是descriptor_index, 其值為0x0006,查一下常量池為#6 = Utf8 I,可知這一句為private int m;
一般來(lái)說(shuō),在decriptor_index后,還有個(gè)屬性集合用于存儲(chǔ)一些額外信息,而0x0000代表沒(méi)有屬性字段。
如果把m字段聲明為private static int m = 123; 則可能多一個(gè)ConstantValue屬性,指向常量值123。
7.方法表集合
方法表集合和字段表集合非常相似,結(jié)構(gòu)也是:
method_info {u2 access_flags;u2 name_index;u2 descriptor_index;u2 attributes_count;attribute_info attributes[attributes_count]; }復(fù)制代碼只不過(guò)在訪問(wèn)標(biāo)志和屬性表集合的可選項(xiàng)有所不同。例如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, 分別對(duì)應(yīng)著synchronized、native、strictfp、abstract這些只能修飾方法的關(guān)鍵字。
現(xiàn)在我們就可以繼續(xù)分析我們程序的二進(jìn)制代碼了。
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 |..../........*..|復(fù)制代碼上一小節(jié)我們剛剛分析到000000f0行的0x0000,接下來(lái)的是0x0002,代表有兩個(gè)方法,接下來(lái)的幾個(gè)字節(jié)是
- 0x0001:訪問(wèn)標(biāo)記是ACC_PUBLIC
- 0x0007:名稱索引指向第7個(gè)常量:
- 0x0008:描述符索引指向第8個(gè)常量:()V
- 0x0001:屬性有一個(gè)
- 0x0009:屬性指向第9個(gè)常量,Code
我們正好有疑問(wèn),方法定義有了,方法體在哪呢,答案就是上面分析的最后一個(gè)Code。下一節(jié)就說(shuō)說(shuō)屬性表集合的各種可能。
8.屬性表集合
屬性表(attribute_info)在前面已經(jīng)多次提及,Class文件、字段表、方法表中都可以攜帶自己的屬性表集合,用于描述某些場(chǎng)景轉(zhuǎn)有的信息。
屬性表并沒(méi)有嚴(yán)格限制順序,只要不與已有屬性名重復(fù),任何人實(shí)現(xiàn)的編譯器都可以添加自己定義的屬性信息,以下是一些預(yù)定義的屬性:
| SourceFile | ClassFile | 記錄源文件的名稱 |
| InnerClasses | ClassFile | 內(nèi)部類列表 |
| EnclosingMethod | ClassFile | 內(nèi)部類才有這個(gè)屬性,用于標(biāo)識(shí)這個(gè)類所在的外圍方法 |
| SourceDebugExtension | ClassFile | 用于存儲(chǔ)額外的調(diào)試信息,JDK1.6中新增 |
| BootstrapMethods | ClassFile | 用于保存invokeddynamic指令引用的引導(dǎo)方法限定符,JDK1.7中新增 |
| ConstantValue | field_info | final關(guān)鍵字定義的常量值 |
| Code | method_info | Java代碼編譯成的字節(jié)碼指令 |
| Exceptions | method_info | 方法拋出的異常 |
| RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations | method_info | 指明哪些參數(shù)是運(yùn)行時(shí)可見(jiàn)的,哪些是運(yùn)行時(shí)不可見(jiàn)的,JDK1.5中新增 |
| AnnotationDefault | method_info | 記錄注解類元素的默認(rèn)值,JDK1.5中新增的 |
| MethodParameters | method_info | 記錄方法的參數(shù)信息,比如它們的名字,訪問(wèn)級(jí)別,JDK1.8新增 |
| Synthetic | ClassFile, field_info, method_info | 表示方法或字段是編譯器自動(dòng)生成的 |
| Deprecated | ClassFile, field_info, method_info | 被聲明為deprecated的字段 |
| Signature | ClassFile, field_info, method_info | 用于支持泛型情況下的方法簽名,在Java語(yǔ)言中,如果任何類、接口、初始化方法或者成員的泛型簽名包含了類型變量或者參數(shù)化類型,則Signature屬性會(huì)為它記錄泛型簽名信息。由于Java的泛型采用擦除法實(shí)現(xiàn),在為了避免類型信息被擦除后導(dǎo)致簽名混亂,需要這個(gè)屬性記錄泛型中的相關(guān)信息。JDK1.5中新增 |
| RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations | ClassFile, field_info, method_info | 為動(dòng)態(tài)注解提供支持,指明哪些是注解是運(yùn)行時(shí)可見(jiàn)的,哪些是運(yùn)行時(shí)不可見(jiàn)的,JDK1.5中新增 |
| LineNumberTable | Code | Java源碼的行號(hào)與字節(jié)碼指令的對(duì)應(yīng)關(guān)系 |
| LocalVariableTable | Code | 方法的局部變量描述 |
| LocalVariableTypeTable | Code | 使用特征簽名代替描述符,是為了引入泛型語(yǔ)法之后能描述泛型參數(shù)化類型而添加,JDK1.5中新增 |
| StackMapTable | Code | 供新的類型檢查驗(yàn)證器(Type Checker)檢查和處理目標(biāo)方法的局部變量和操作棧所需要的類型是否匹配,JDK1.6新增 |
| RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations | ClassFile, field_info, method_info, Code | 記錄運(yùn)行時(shí)類型上注解的可見(jiàn)性,也包括運(yùn)行時(shí)類型參數(shù)的注解的可見(jiàn)性 |
下面具體說(shuō)一說(shuō)一些比較重要的屬性:
Code屬性
首先來(lái)看Code屬性的結(jié)構(gòu):
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]; }復(fù)制代碼- attribute_name_index: 占兩個(gè)字節(jié),指向CONSTANT_Utf8_info常量,表示屬性名,這里固定是"Code"
- attribute_length:屬性值的長(zhǎng)度,由于attribute_name_index和attribute_length占6個(gè)字節(jié),所以attribute_length為屬性表長(zhǎng)度減6
- max_statck: 操作數(shù)棧深度的最大值,在方法執(zhí)行時(shí),操作數(shù)棧不能超過(guò)這個(gè)值
- max_locals: 局部變量所需的存儲(chǔ)空間。max_locals的單位是Slot,Slot是虛擬機(jī)為局部變量分配的最小單位,對(duì)于byte,char,float,int,short,boolean和returnAddress等長(zhǎng)度不超過(guò)32位的數(shù)據(jù)類型,都只占一個(gè)slot,而double和long 這種64為的數(shù)據(jù)都是需要占用兩個(gè)slot。方法參數(shù)(包括隱藏的this)、異常處理器的參數(shù)、方法體定義的局部變量都需要局部變量表來(lái)存放。但是max_locals并不是所有局部變量所占的slot之和,因?yàn)閟lot可以重用,當(dāng)一個(gè)變量超出作用域了,該slot又會(huì)給別的局部變量使用,編譯器會(huì)根據(jù)作用域計(jì)算max_locals。
- code_length: 編譯器編譯后的字節(jié)碼長(zhǎng)度
- code: 用于存儲(chǔ)字節(jié)碼指令的一系列字節(jié)流,每個(gè)指令是一個(gè)u1類型的單字節(jié),當(dāng)虛擬機(jī)讀到該字節(jié)時(shí),就可以知道是什么指令,知道是什么指令,就知道指令需要什么操作數(shù),繼續(xù)讀就可以了,這里類似于匯編,u1的取值范圍是0~255,可以表達(dá)256條指令。Java虛擬機(jī)規(guī)范中定義了約200條指令,參看Instructions。關(guān)于這部分內(nèi)容以后再寫博客介紹了。
- exception_table_length:異常表的長(zhǎng)度
- exception_table: 異常表對(duì)于Code來(lái)說(shuō)并不是必須存在的,所以上述長(zhǎng)度也可以為0,異常表有4個(gè)屬性,代表著如果在start_pc到end_pc之間出現(xiàn)catch_type類型的異常,就跳轉(zhuǎn)到handler_pc所指向的行處理。
Exceptions屬性
Exceptions屬性在方法表中與Code屬性平級(jí),注意和上面Code中的異常表不同,Exceptions屬性的作用是列出方法可能拋出的異常,Exceptions屬性表的結(jié)構(gòu):
Exceptions_attribute {u2 attribute_name_index;u4 attribute_length;u2 number_of_exceptions;u2 exception_index_table[number_of_exceptions]; }復(fù)制代碼- number_of_exceptions: 可能拋出的異常種類的個(gè)數(shù)
- exception_index_table:指向常量池中CONSTANT_Class_info的索引
LineNumberTable屬性
LineNumber用來(lái)記錄Java源碼與字節(jié)碼行號(hào)之間的對(duì)應(yīng)關(guān)系,我們?cè)诰幾g代碼時(shí)也可以使用-g: none或-g: line來(lái)取消生成這個(gè)屬性,不過(guò)在調(diào)試代碼時(shí)就看不到行號(hào)了,也無(wú)法打斷點(diǎn)。
LineNumberTable的數(shù)據(jù)結(jié)構(gòu)如下:
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]; }復(fù)制代碼我們主要看line_number_table,start_pc是字節(jié)碼行號(hào),line_number是Java源碼行號(hào)。
LocalVariableTable屬性
LocalVariableTable屬性用于描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的關(guān)系,我們?cè)诰幾g代碼時(shí)也可以使用-g: none或-g: vars來(lái)取消生成這個(gè)屬性,但是如果取消的話,IDE會(huì)用arg0,arg1這樣的參數(shù)來(lái)取代原有的參數(shù)名,導(dǎo)致調(diào)試時(shí)不清晰。
LocalVariableTable的數(shù)據(jù)結(jié)構(gòu)如下:
主要介紹local_variable_table:
- start_pc和length: 分別代表了這個(gè)局部變量的生命周期開(kāi)始的字節(jié)碼偏移量以及作用范圍覆蓋的長(zhǎng)度
- name_index和descriptor_index:分別指向代表局部變量名稱和局部變量描述符的常量
- index: 是該局部變量在局部變量表中的slot位置,如果變量時(shí)double 或者long類型的,占用的slot為index和index+1兩個(gè)。
ConstantValue屬性
ConstantValue是一個(gè)定長(zhǎng)屬性,用來(lái)通知虛擬機(jī)為靜態(tài)變量賦值,如果同時(shí)定義了int x=3;和static int y=3;則虛擬機(jī)為x,y賦值的時(shí)機(jī)不同,對(duì)于x,是在實(shí)例構(gòu)造器<init>中進(jìn)行的,而static類型的變量,則會(huì)在類構(gòu)造器<clinit>方法中或者使用ConstantValue屬性。
目前javac編譯器的規(guī)則是,如果同時(shí)有final和static修飾,則是使用ConstantValue屬性,只有static時(shí),并且變量類型是基本類型或者String時(shí),就會(huì)在<clinit>中進(jìn)行初始化。
InnerClasses屬性
如果類中定義了內(nèi)部類,則會(huì)使用InnerClasses屬性來(lái)記錄內(nèi)部類和宿主的關(guān)系。
InnerClasses的數(shù)據(jù)結(jié)構(gòu)如下:
還是只看classes字段,inner_class_info_index指向內(nèi)部類的符號(hào)引用,outer_class_info_index指向宿主類的符號(hào)引用,inner_name_index指向內(nèi)部類的名稱,如果是匿名內(nèi)部類,則為0,inner_class_access_flags是內(nèi)部類的訪問(wèn)標(biāo)志,見(jiàn)下表:
| 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. |
還有其它的一些屬性,如果想了解,可以看一下參考資料。
參考資料:
總結(jié)
以上是生活随笔為你收集整理的深入理解JVM类文件格式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Class类文件的结构
- 下一篇: MHA masterha_check_s