從學(xué)習(xí)Java語言的第一天起辩昆,我們就被告知與其他語言相比阅酪,Java的一大特點(diǎn)在于它的平臺無關(guān)性,即Write Once, Run Everywhere. 而構(gòu)成平臺無關(guān)性的基石就在于所有JVM都采用了字節(jié)碼作為它們的程序存儲格式卤材,因此今天主要就分析一下class文件的結(jié)構(gòu)遮斥。
知識準(zhǔn)備
在詳細(xì)分析class文件結(jié)構(gòu)之前峦失,我們需要了解一些基本概念:
- class文件以8字節(jié)為基本單位來進(jìn)行存儲扇丛,中間沒有任何分隔符;
- 當(dāng)數(shù)據(jù)項(xiàng)需要占用的空間大于8字節(jié)時尉辑,會按照高位在前的方式來進(jìn)行分割帆精;
- class文件只有兩種數(shù)據(jù)類型:無符號數(shù)、表隧魄;
- 無符號數(shù)屬于基本數(shù)據(jù)類型卓练,以u1、u2购啄、u4襟企、u8分別代表1個字節(jié)、2個字節(jié)狮含、4個字節(jié)和8個字節(jié)的無符號數(shù)顽悼;
- 表是由多個無符號數(shù)或者其它表作為數(shù)據(jù)項(xiàng)構(gòu)成的符合數(shù)據(jù)類型,表名習(xí)慣性都以 _info 結(jié)尾几迄。
因此本質(zhì)上整個class文件就是一張表蔚龙,它由以下數(shù)據(jù)項(xiàng)構(gòu)成:
類型 | 名稱 | 數(shù)量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔數(shù) |
u2 | minor_version | 1 | 次版本號 |
u2 | major_version | 1 | 主版本號 |
u2 | constant_pool_count | 1 | 常量個數(shù) |
cp_info | constant_pool | constant_pool_count - 1 | 具體常量 |
u2 | access_flags | 1 | 訪問標(biāo)志 |
u2 | this_class | 1 | 類索引 |
u2 | super_class | 1 | 父類索引 |
u2 | interfaces_count | 1 | 接口索引 |
u2 | interfaces | interfaces_count | 具體接口 |
u2 | fields_count | 1 | 字段個數(shù) |
field_info | fields | fields_count | 具體字段 |
u2 | methods_count | 1 | 方法個數(shù) |
method_info | methods | methods_count | 具體方法 |
u2 | attributes_count | 1 | 屬性個數(shù) |
attribute_info | attributes | attributes_count | 具體屬性 |
可以看到炉菲,這16種數(shù)據(jù)項(xiàng)大致可以分為3類:
- 3個描述文件屬性的數(shù)據(jù)項(xiàng):魔數(shù)和主次版本號
- 11個描述類屬性的數(shù)據(jù)項(xiàng):類嗦枢、字段、方法等信息
- 2個描述代碼屬性的數(shù)據(jù)xiang:
接下來我們就逐一來看看這些數(shù)據(jù)項(xiàng)的含義峰锁。整個分析過程我們將以下面這段代碼對應(yīng)的class文件為基礎(chǔ):
public class JavaTest {
private static String name = "JVM";
public static void main(String[] args) {
System.out.println("Hello " + name);
}
}
1.魔數(shù)與版本
每個class文件的頭4個字節(jié)稱為魔數(shù)解孙,用于確定這個文件是否能被虛擬機(jī)所接受坑填。class文件的魔數(shù)值為CAFEBABE。
第5弛姜、6字節(jié)為次版本號脐瑰,7、8字節(jié)為主版本號娱据。Java的主版本號從45開始蚪黑,JDK1.1之后每個大版本發(fā)布盅惜,主版本號加1。高版本的jdk能前向兼容之前版本的class文件忌穿,但不能運(yùn)行以后版本的class文件抒寂。
從圖1可以看到,次版本號為0000掠剑,主版本號為0031屈芜,這說明該class文件可以被1.5及以后版本的jdk運(yùn)行。
2.常量池
緊接著主版本號之后的是常量池入口朴译,由于常量池中常量數(shù)量不固定井佑,因此入口使用第一個u2類型的數(shù)據(jù)代表常量池計(jì)數(shù)值,該計(jì)數(shù)器從1開始眠寿。圖1中常量池計(jì)數(shù)值為0034躬翁,代表常量池中一共有51個常量。
常量池中每一個常量都是一個表盯拱,jdk1.7之后一共有14種類型的常量盒发,他們對應(yīng)著14個不同結(jié)構(gòu)的表,但這14個表都有一個共同特點(diǎn):那就是表開始的第一位是一個u1類型的標(biāo)志位狡逢,代表當(dāng)前常量屬于哪種常量類型宁舰。其取值和含義如下表所示:
類型 | 標(biāo)志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8編碼的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮點(diǎn)型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點(diǎn)型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類方法的符號引用 |
CONSTANT_InterfaceMehtodref_info | 11 | 接口方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符號引用 |
CONSTANT_MethodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 動態(tài)方法調(diào)用點(diǎn) |
這14種常量的結(jié)構(gòu)如下表所示:
有了這些基礎(chǔ),我們繼續(xù)分析前面提到的class文件:
第一個u1類型的變量代表常量類型為0A奢浑,對應(yīng)的表為CONSTANT_Methodref_info蛮艰,表示方法引用,緊接著一個u2類型的變量000C,它表示聲明該方法的類描述符為常量池匯中的第12個常量雀彼,第二個u2類型的變量001D表示指向該方法名稱及類型的描述符為常量池中的第29個常量壤蚜。
按照同樣的方式,下圖給出了前面14個常量的字節(jié)碼详羡,其中前面12個都是指向了常量池中的其它常量仍律,第13、14個常量是兩個類型為1(即UTF-8編碼的常量)实柠,對應(yīng)的英文字符分別為name水泉、Ljava/lang/String.
剩下其他常量的劃分方式是類似的,事實(shí)上窒盐,jdk已經(jīng)為我們提供了專門用于分析class文件的工具javap草则,利用javap -v JavaTest.class得到常量池中的52個常量如下,可以看到蟹漓,前面14個常量的劃分與我們之前分析的完全一致炕横。
bogon:Downloads shiyangsheng$ javap -v JavaTest.class
Classfile /Users/shiyangsheng/Downloads/JavaTest.class
Last modified 2018-3-17; size 842 bytes
MD5 checksum fbb2370c6b7413a0636806a0e492224a
Compiled from "JavaTest.java"
public class com.youzan.shys.advice.JavaTest
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#29 // java/lang/Object."<init>":()V
#2 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #32 // java/lang/StringBuilder
#4 = Methodref #3.#29 // java/lang/StringBuilder."<init>":()V
#5 = String #33 // Hello
#6 = Methodref #3.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Fieldref #11.#35 // com/youzan/shys/advice/JavaTest.name:Ljava/lang/String;
#8 = Methodref #3.#36 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = String #39 // JVM
#11 = Class #40 // com/youzan/shys/advice/JavaTest
#12 = Class #41 // java/lang/Object
#13 = Utf8 name
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/youzan/shys/advice/JavaTest;
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8 <clinit>
#27 = Utf8 SourceFile
#28 = Utf8 JavaTest.java
#29 = NameAndType #15:#16 // "<init>":()V
#30 = Class #42 // java/lang/System
#31 = NameAndType #43:#44 // out:Ljava/io/PrintStream;
#32 = Utf8 java/lang/StringBuilder
#33 = Utf8 Hello
#34 = NameAndType #45:#46 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #13:#14 // name:Ljava/lang/String;
#36 = NameAndType #47:#48 // toString:()Ljava/lang/String;
#37 = Class #49 // java/io/PrintStream
#38 = NameAndType #50:#51 // println:(Ljava/lang/String;)V
#39 = Utf8 JVM
#40 = Utf8 com/youzan/shys/advice/JavaTest
#41 = Utf8 java/lang/Object
#42 = Utf8 java/lang/System
#43 = Utf8 out
#44 = Utf8 Ljava/io/PrintStream;
#45 = Utf8 append
#46 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#47 = Utf8 toString
#48 = Utf8 ()Ljava/lang/String;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Ljava/lang/String;)V
由此可見,常量池在class文件中占據(jù)了絕大部分內(nèi)容(中間用紅框框出來的就是常量池內(nèi)容):
3.訪問標(biāo)志
緊接著常量池之后的兩個字節(jié)表示訪問標(biāo)志葡粒,主要是用來標(biāo)記類或者接口層次的一些屬性份殿。目標(biāo)之定義了16個標(biāo)志位中的8位膜钓,沒有使用到的一律為0。 具體標(biāo)志位如下表:
標(biāo)志名稱 | 標(biāo)志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public類型 |
ACC_FINAL | 0x0010 | 是否為final類型 |
ACC_SUPER | 0x0020 | 是否允許使用invokespcial字節(jié)碼指令的新語義卿嘲,jdk1.0.2之后編譯出來的類颂斜,此標(biāo)志都為真 |
ACC_INTERFACE | 0x0200 | 是否為接口 |
ACC_ABSTRACT | 0x0400 | 是否為abstract類型(對接口和抽象類來說,此標(biāo)志都為真) |
ACC_SYNTHETIC | 0x1000 | 標(biāo)識這個類并非由用戶代碼產(chǎn)生 |
ACC_ANNOTATION | 0x2000 | 是否是注解 |
ACC_ENUM | 0x4000 | 是否是枚舉 |
顯然拾枣,對JavaTest類而言沃疮,只有ACC_PUBLIC、ACC_SUPER兩個標(biāo)志應(yīng)該為真梅肤,因此access_flags=0x0021.
4.類索引司蔬、父類索引和接口索引集合
在訪問標(biāo)志之后,有3個用來確定一個類的繼承關(guān)系的數(shù)據(jù)姨蝴,按先后順序分別是:
- 類索引:用于確定類的全限定名
- 父類索引:用于確定父類的全限定名
- 接口索引:用于描述類實(shí)現(xiàn)了哪些接口
它們在class文件中的位置如下:
可見俊啼,類索引為11,父類索引為12似扔,接口索引集合大小為0吨些,根據(jù)前面得到的常量池搓谆,可以知道第11炒辉、12個常量為:
...
#11 = Class #40 // com/youzan/shys/advice/JavaTest
#12 = Class #41 // java/lang/Object
...
#40 = Utf8 com/youzan/shys/advice/JavaTest
#41 = Utf8 java/lang/Object
...
5.字段表集合
在接口索引之后是字段表集合,字段表用來描述接口或者類中聲明的變量泉手。它包括類級變量和實(shí)例級變量黔寇,但是不包括局部變量以及從父類和接口中繼承而來的字段。字段表的格式如下:
類型 | 名稱 | 數(shù)量 | 含義 |
---|---|---|---|
u2 | access_flags | 1 | 字段修飾符 |
u2 | name_index | 1 | 字段和方法簡單名稱在常量池中的引用 |
u2 | descriptor_index | 1 | 字段和方法描述符在常量池中的引用 |
u2 | attributes_count | 1 | 描述字段額外信息屬性的個數(shù) |
attribute_info | attributes | attributes_count | 具體描述字段的額外信息屬性 |
5.1字段修飾符
字段修飾符與類中的訪問標(biāo)志很類似斩萌,用來描述字段的一些屬性:
標(biāo)志名稱 | 標(biāo)志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public類型 |
ACC_PRIVATE | 0x0002 | 是否為private類型 |
ACC_PROTECTED | 0x0004 | 是否為protected類型 |
ACC_STATIC | 0x0008 | 是否為static類型 |
ACC_FINAL | 0x0010 | 是否為final類型 |
ACC_VOLATILE | 0x0040 | 是否volatile類型 |
ACC_TRANSIENT | 0x0080 | 是否transient類型 |
ACC_SYNTHETIC | 0x1000 | 是否由編譯器自動產(chǎn)生 |
ACC_ENUM | 0x4000 | 是否enum類型 |
5.2全限定名
把類全路徑中的.替換為/缝裤,同時在最后加入一個;即可。
5.3簡單名稱
簡單名稱指的是沒有類型和修飾符的字段或者方法名稱颊郎。
5.4描述符
描述符用來描述字段的數(shù)據(jù)類型憋飞、方法的參數(shù)列表和返回值。其中基本類型字段的描述符用一個大寫字母來表示姆吭,而對象類型則用字符L加上對象類型的全限定名來表示榛做。具體如下表:
描述符 | 含義 |
---|---|
B | 基本類型byte |
C | 基本類型char |
D | 基本類型double |
F | 基本類型float |
I | 基本類型int |
J | 基本類型long |
S | 基本類型short |
Z | 基本類型boolean |
V | 基本類型void |
L | 對象類型,如Ljava/lang/Object |
對于數(shù)組類型内狸,每一個維度都是用一個前置的“[”來描述检眯,如java.lang.String[][]類型的二位數(shù)組將被記錄為[[java/lang/String;
描述方法時,將按照先參數(shù)列表昆淡、后返回值的順序來描述锰瘸。其中參數(shù)列表嚴(yán)格按照參數(shù)的順序放在一組小括號()之內(nèi)。例如方法java.lang.String.toString()的描述符為()Ljava/lang/String;
了解了這幾個概念之后昂灵,我們回到JavaTest的class文件:
- fields_count=0x0001表明這個類只有一個字段表數(shù)據(jù)避凝;
- access_flags=0x000A表明ACC_PRIVATE與ACC_STATIC標(biāo)志位為1真舞萄,其它標(biāo)志位為0;
- name_index=0x000D表明字段簡單名稱為常量池中的第13個常量管削,也就是name鹏氧;
- descriptor=0x000E表明字段描述符為常量池中的第14個常量,也就是Ljava/lang/String;
- attributes_count=0x0000表明字段額外屬性個數(shù)為0佩谣;
由此可以反過來得到該類的一個屬性為 private static String name;
6.方法表集合
對方法描述的方式與對字段描述的方式基本一致把还,方法表的結(jié)構(gòu)也與字段表的結(jié)構(gòu)完全一致,不同之處在于方法的訪問標(biāo)志與字段的訪問標(biāo)志有所區(qū)別茸俭。例如volatile與transient不能修飾方法吊履,但是方法卻有synchronized、native调鬓、strictfp和abstract等屬性艇炎。其具體訪問標(biāo)志如下:
標(biāo)志名稱 | 標(biāo)志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public類型 |
ACC_PRIVATE | 0x0002 | 是否為final類型 |
ACC_PROTECTED | 0x0004 | 是否為protected類型 |
ACC_STATIC | 0x0008 | 是否為static類型 |
ACC_FINAL | 0x0010 | 是否為final類型 |
ACC_SYNCHRONIZED | 0x0020 | 是否synchronized類型 |
ACC_BRIDGE | 0x0040 | 是否橋接方法 |
ACC_VARARGS | 0x0080 | 是否接收不定參數(shù) |
ACC_NATIVE | 0x0100 | 是否native方法 |
ACC_ABSTRACT | 0x0400 | 是否abstract |
ACC_STRICTFP | 0x0800 | 是否strictfp |
ACC_SYNTHETIC | 0x1000 | 是否由編譯器自動產(chǎn)生 |
讓我們繼續(xù)回到class文件:
6.1 第一個方法
- methods_count=0x0003表明該類有3個方法;
- 第一個方法的access_flags=0x0001表明只有ACC_PUBLIC標(biāo)志位為真腾窝;
- name_index=0x000F表明方法簡單名稱為常量池中的第15個常量缀踪,也就是<init>;
- descriptor=0x0010表明方法修飾符為常量池中的第16個常量虹脯,也就是()V驴娃;
- attributes_count=0x0001表明第一個方法有一個額外屬性,且索引值就是其后的0x0011,也就是常量池中的第17個常量Code循集。
- Code屬性是該方法的具體字節(jié)碼描述唇敞。
由此得到第一個方法為public void init(),這個方法是編譯器自動添加的實(shí)例構(gòu)造器方法咒彤。
Code屬性也是class文件中最重要的一個屬性疆柔,如果把一個Java程序中的信息分為代碼(方法體里的代碼)和元數(shù)據(jù)(描述類、字段镶柱、方法的其他信息)兩部分旷档,那么Code屬性描述的就是代碼的信息,其它所有數(shù)據(jù)都用于描述元數(shù)據(jù)歇拆。由于Code數(shù)據(jù)極其重要也相對復(fù)雜鞋屈,我將在另外一篇文章中單獨(dú)介紹,這里直接給出init()方法的Code屬性在class文件中的表示(簡單來說查吊,前面4個字節(jié)0000 002F表示屬性值的長度谐区,也就是47個字節(jié),也就是說后續(xù)47個字節(jié)都是Code屬性的內(nèi)容):
6.2 第二個方法
第一個方法的Code屬性后面緊跟著的是第二個方法的描述逻卖,同樣的分析方法宋列,不難得出第二個方法為public static void main(String[]);其Code屬性值的長度為0000 004A,也就是74個字節(jié)评也。
6.3 第三個方法
同樣炼杖,很容易得到第三個方法為static clinit void();這個方法是編譯器自動添加的類構(gòu)造器方法灭返,其Code屬性值的長度為0000 001E,也就是30個字節(jié)坤邪。
實(shí)際上熙含,這個分析與javap得到的結(jié)果也是一致的。
{
public com.youzan.shys.advice.JavaTest();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/youzan/shys/advice/JavaTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String Hello
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: getstatic #7 // Field name:Ljava/lang/String;
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
LineNumberTable:
line 14: 0
line 15: 27
LocalVariableTable:
Start Length Slot Name Signature
0 28 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #10 // String JVM
2: putstatic #7 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
}
7.屬性表集合
屬性表集合用于描述某些場景的專有信息艇纺,它一共有21個屬性怎静,屬性表結(jié)構(gòu)如下:
類型 | 名稱 | 數(shù)量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | info | attribute_length |
由于涉及屬性太多,這里也不再展開黔衡,只是簡單說明下之類的屬性代表什么意思蚓聘。從class文件可以看出,該類的屬性表集合只有一個元素盟劫,001B表示常量池中的第27個常量夜牡,也就是SourceFile,001C表示常量池中的第28個常量侣签,也就是JavaTest.java塘装,也就是說,SourceFile屬性記錄了生成這個class文件的源碼文件的名稱影所。