Java代碼必須要被編譯成class文件后薪贫,虛擬機才能夠加載運行艘狭,要搞清楚Java的類加載機制,首先必須要理解Class文件的內(nèi)部結(jié)構(gòu)囚霸。
本文參考了周志明所著《深入理解java虛擬機》一書腰根,并結(jié)合自身實踐而寫。
1.Class文件基本結(jié)構(gòu)概述
- Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進制流拓型,當遇到需要8位字節(jié)以上空間的數(shù)據(jù)項時额嘿,則會按照高位在前的方式分隔成若干個8位字節(jié)進行存儲。
- Class文件由無符號數(shù)和表構(gòu)成劣挫。
- 無符號數(shù):以u1册养、u2、u4压固、u8分別代表1個字節(jié)球拦、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù)帐我,可以用來描述數(shù)字坎炼、索引引用、數(shù)量值拦键、按照UTF-8編碼構(gòu)成的字符串值谣光。
- 表:由多個無符號數(shù)或其他表作為數(shù)據(jù)項構(gòu)成的復雜數(shù)據(jù)類型,所有表都習慣性地以“_info”結(jié)尾芬为。
Class文件格式如下:
類型 | 描述 | 備注 |
---|---|---|
u4 | magic | 魔數(shù):0xCAFEBABE |
u2 | minor_version | 小版本號 |
u2 | major_version | 主版本號 |
u2 | constant_pool_count | 常量池大小萄金,從1開始 |
cp_info | constant_pool[constant_pool_count - 1] | 常量池信息 |
u2 | access_flags | 訪問標志 |
u2 | this_class | 類索引 |
u2 | super_class | 父類索引 |
u2 | interfaces_count | 接口個數(shù) |
u2 | interfaces[interfaces_count] | 接口類索引信息 |
u2 | fields_count | 字段數(shù) |
field_info | fields[fields_count] | 字段表信息 |
u2 | methods_count | 方法數(shù) |
method_info | methods[methods_count] | 方法表信息 |
u2 | attributes_count | 屬性個數(shù) |
attribute_info | attributes[attributes_count] | 屬性表信息 |
2.一個典型的Class文件
//接口類
public interface Car {
void drive();
}
//實現(xiàn)類
public class BMWCar implements Car{
private String name;
public BMWCar() {
name = "寶馬";
}
@Override
public void drive() {
System.out.println("BMW car drive." + name);
}
}
采用javac命令編譯以上代碼后蟀悦,用Sublime編輯器打開BMWCar.class文件,內(nèi)容如下:
cafe babe 0000 0034 002f 0a00 0c00 1708
0018 0900 0b00 1909 001a 001b 0700 1c0a
0005 0017 0800 1d0a 0005 001e 0a00 0500
1f0a 0020 0021 0700 2207 0023 0700 2401
0004 6e61 6d65 0100 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0006 3c69
6e69 743e 0100 0328 2956 0100 0443 6f64
6501 000f 4c69 6e65 4e75 6d62 6572 5461
626c 6501 0005 6472 6976 6501 000a 536f
7572 6365 4669 6c65 0100 0b42 4d57 4361
722e 6a61 7661 0c00 1000 1101 0006 e5ae
9de9 a9ac 0c00 0e00 0f07 0025 0c00 2600
2701 0017 6a61 7661 2f6c 616e 672f 5374
7269 6e67 4275 696c 6465 7201 000e 424d
5720 6361 7220 6472 6976 652e 0c00 2800
290c 002a 002b 0700 2c0c 002d 002e 0100
0642 4d57 4361 7201 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 0343 6172
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0006 6170 7065 6e64 0100 2d28 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
294c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 4275 696c 6465 723b 0100 0874 6f53
7472 696e 6701 0014 2829 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 136a
6176 612f 696f 2f50 7269 6e74 5374 7265
616d 0100 0770 7269 6e74 6c6e 0100 1528
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 2956 0021 000b 000c 0001 000d 0001
0002 000e 000f 0000 0002 0001 0010 0011
0001 0012 0000 002b 0002 0001 0000 000b
2ab7 0001 2a12 02b5 0003 b100 0000 0100
1300 0000 0e00 0300 0000 0600 0400 0700
0a00 0800 0100 1400 1100 0100 1200 0000
3900 0300 0100 0000 1db2 0004 bb00 0559
b700 0612 07b6 0008 2ab4 0003 b600 08b6
0009 b600 0ab1 0000 0001 0013 0000 000a
0002 0000 000c 001c 000d 0001 0015 0000
0002 0016
3.魔數(shù)
4.版本號
次版本號是:0000
主版本號是:0034氧敢,十進制是52日戈,表示采用的是jdk1.8
5.常量池
版本號后面是常量池,常量池中常量的數(shù)量是不固定的福稳,所以常量池的入口處有一個u2類型的數(shù)據(jù),表示常量池中常量的數(shù)值大小瑞侮。0x002f十進制數(shù)值是47的圆,表示常量池常量數(shù)為46(注意常量池計數(shù)是從1而不是0開始),使用“javap -v BMWCar”命令可以查看Class文件的信息如下:
Last modified 2017-11-10; size 644 bytes
MD5 checksum ac6d7477d45479490e4ea3f660b1dcdd
Compiled from "BMWCar.java"
public class BMWCar implements Car
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#23 // java/lang/Object."<init>":()V
#2 = String #24 // 寶馬
#3 = Fieldref #11.#25 // BMWCar.name:Ljava/lang/String;
#4 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Class #28 // java/lang/StringBuilder
#6 = Methodref #5.#23 // java/lang/StringBuilder."<init>":()V
#7 = String #29 // BMW car drive.
#8 = Methodref #5.#30 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#9 = Methodref #5.#31 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #32.#33 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #34 // BMWCar
#12 = Class #35 // java/lang/Object
#13 = Class #36 // Car
#14 = Utf8 name
#15 = Utf8 Ljava/lang/String;
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 drive
#21 = Utf8 SourceFile
#22 = Utf8 BMWCar.java
#23 = NameAndType #16:#17 // "<init>":()V
#24 = Utf8 寶馬
#25 = NameAndType #14:#15 // name:Ljava/lang/String;
#26 = Class #37 // java/lang/System
#27 = NameAndType #38:#39 // out:Ljava/io/PrintStream;
#28 = Utf8 java/lang/StringBuilder
#29 = Utf8 BMW car drive.
#30 = NameAndType #40:#41 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#31 = NameAndType #42:#43 // toString:()Ljava/lang/String;
#32 = Class #44 // java/io/PrintStream
#33 = NameAndType #45:#46 // println:(Ljava/lang/String;)V
#34 = Utf8 BMWCar
#35 = Utf8 java/lang/Object
#36 = Utf8 Car
#37 = Utf8 java/lang/System
#38 = Utf8 out
#39 = Utf8 Ljava/io/PrintStream;
#40 = Utf8 append
#41 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = Utf8 toString
#43 = Utf8 ()Ljava/lang/String;
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
#46 = Utf8 (Ljava/lang/String;)V
{
public BMWCar();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String 寶馬
7: putfield #3 // Field name:Ljava/lang/String;
10: return
LineNumberTable:
line 6: 0
line 7: 4
line 8: 10
public void drive();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #5 // class java/lang/StringBuilder
6: dup
7: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
10: ldc #7 // String BMW car drive.
12: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #3 // Field name:Ljava/lang/String;
19: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 12: 0
line 13: 28
}
SourceFile: "BMWCar.java"
從這里也可以看到半火,常量池的常量數(shù)為46越妈。常量池中主要存放2大類常量:字面量(Literal)和符號引用(Symbolic References)。
字面量主要指文本字符串钮糖、被聲明為final的常量值梅掠。
符號引用主要包括以下3類:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
常量池中的每個常量都是一個表,共有11種不同的表結(jié)構(gòu)店归,它們有一個共同的特點阎抒,就是表開始的第一位都是一個u1類型的標志位(tag,取值為1到12消痛,缺少標志為2的數(shù)據(jù)類型)且叁。
tag表示的數(shù)據(jù)類型如下表所示:
類型 | 標志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8編碼的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量,boolean秩伞、byte逞带、char、short等類型都用int存放 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符號引用 |
從以上表中可以看到纱新,1-12除了類型2之外展氓,共11種表數(shù)據(jù)類型,以下是一份常量池數(shù)據(jù)結(jié)構(gòu)總表:
接下來我們分析常量池里的第一個常量:
這里可以看到脸爱,第一個常量的tag = 10遇汞,從前面的常量數(shù)據(jù)結(jié)構(gòu)表中可以看到,10表示該常量為CONSTANT_Methodref_info簿废,表示方法的符號引用勺疼,接下來有兩個字節(jié)class_index = 12,指向常量池中索引值為12的常量CONSTANT_Class_info捏鱼,再接下來2個字節(jié)name_and_type_index = 23执庐,指向常量池中索引值為23的CONSTANT_NameAndType_info,與前面我們采用java命令查看class的信息是一致的导梆。
#1 = Methodref #12.#23 // java/lang/Object."<init>":()V
#12 = Class #35 // java/lang/Object
#23 = NameAndType #16:#17 // "<init>":()V
這里也可以看到第1個常量是一個方法的符號引用轨淌,它指向了第12個常量和第23個常量迂烁,第12個常量表示類的符號引用,它指向了第35個常量递鹉,第23個常量又指向了第16盟步、17個常量。
這樣一個一個常量去分析躏结,我們就可以計算出整個常量池在class文件中所占的字節(jié)空間却盘,如下圖所示:
6.訪問標志
常量池之后,是2個字節(jié)來表示訪問標志媳拴,用于識別一些類或者接口層次的訪問信息黄橘。
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public類型 |
ACC_FINAL | 0x0010 | 是否被聲明為final,只有類可以設(shè)置 |
ACC_SUPER | 0x0020 | JDK1.0.2以后這個標志都為真 |
ACC_INTERFACE | 0x0200 | 標識這是一個接口 |
ACC_ABSTRACT | 0x0400 | 是否為abstract類型屈溉,對于接口或抽象類來說塞关,此標志值為真,其他類值為假 |
ACC_SYNTHETIC | 0x1000 | 標識這個類并非由用戶代碼產(chǎn)生 |
ACC_ANNOTATION | 0x2000 | 標識這是一個注解 |
ACC_ENUM | 0x4000 | 標識這是一個枚舉 |
針對BMWCar這個類來說子巾,其訪問標志應該是ACC_PUBLIC帆赢、ACC_SUPER這2個標志為真,所以其值為 0x0001 | 0x0020 = 0x0021线梗。
7.類索引椰于、父類索引
訪問標志后的2個字節(jié)000b = 11,指向常量池里的第11個常量仪搔,接下來的000c = 12廉羔,指向常量池里的第12個常量(該類的父類是Object)
8.接口索引
在本例子中,只有一個接口僻造,0x000d十進制就是13憋他,指向常量池中第13個常量,表示接口信息髓削。
9.字段表
接口索引后面緊跟著的是字段表信息竹挡,字段表的入口前2個字節(jié)表示字段的個數(shù),在本例子中只定義了個一個字段立膛,所以其值為0x0001揪罕,后面緊跟著的是該字段的描述表。字段信息結(jié)構(gòu)表如下:
類型 | 描述 | 備注 |
---|---|---|
u2 | access_flags | 記錄字段的訪問標志 |
u2 | name_index | 常量池中的索引項宝泵,指定字段的名稱 |
u2 | descriptor_index | 常量池中的索引項好啰,指定字段的描述符 |
u2 | attributes_count | attributes包含的項目數(shù) |
attribute_info | attributes[attributes_count] |
字段訪問標志:
權(quán)限名稱 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static,靜態(tài) |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | volatile儿奶,不可和ACC_FIANL一起使用 |
ACC_TRANSIENT | 0x0080 | 在序列化中被忽略的字段 |
ACC_SYNTHETIC | 0x1000 | 由編譯器產(chǎn)生框往,不存在于源代碼中 |
ACC_ENUM | 0x4000 | enum |
緊隨access_flags標志的是name_index和descriptor_index,他們都是對常量池的引用闯捎。name_index代表著字段的簡單名稱椰弊,descriptor_index代表著字段的描述符许溅。描述符的作用是用來描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量秉版、類型以及順序)和返回值贤重。
描述符標識字符含義:
標識字符 | 含義 |
---|---|
B | 基本類型byte |
C | 基本類型char |
D | 基本類型double |
F | 基本類型float |
I | 基本類型int |
J | 基本類型long |
S | 基本類型short |
Z | 基本類型boolean |
V | 特殊類型void |
L | 對象類型,如Ljava/lang/Object; |
[ | 數(shù)組類型清焕,多個維度則有多個[ |
用描述符來描述方法時并蝗,按照先參數(shù)列表后返回值的順序描述,參數(shù)列表按照參數(shù)的嚴格順序放在一組“()”之內(nèi)秸妥。
例如方法int getAge()的描述符為“()I”滚停,方法void print(String msg)的描述符為“(Ljava/lang/String;)V”,方法int indexOf(int index, char[] arr)的描述符為“(I[C)I”筛峭。
再來看我們這個字節(jié)碼:
我們來具體分析下字節(jié)碼:
- 前兩個字節(jié)0x0001表示字段計數(shù)值為1铐刘,即該類有1個字段陪每;
- 0x0002是第一個字段的訪問標志影晓,這里為private;
- 0x000e是name_index檩禾,查找常量池可知其值為“name”挂签;
- 0x000f是descriptor_index,查找常量池可知其值為“Ljava/lang/String;”盼产,至此通過字節(jié)碼分析饵婆,我們可以反推出這個字段的定義為:private String name;
- 0x0000表示attribute_count = 0戏售,說明本字段沒有額外的描述信息侨核。但是如果該字段的聲明為“private static final String name = "123"”,那就會存在一項名為ConstantValue的屬性灌灾,其值指向常量“123”搓译;
10.方法表
方法表的結(jié)構(gòu)與字段表的結(jié)構(gòu)是一樣的。
類型 | 描述 | 備注 |
---|---|---|
u2 | access_flags | 記錄方法的訪問標志 |
u2 | name_index | 常量池中的索引項锋喜,指定方法的名稱 |
u2 | descriptor_index | 常量池中的索引項些己,指定方法的描述符 |
u2 | attributes_count | attributes包含的項目數(shù) |
attribute_info | attributes[attributes_count] |
再來看看我們這個例子:
- 前2個字節(jié)0x0002表示有2個方法,一個是編譯器添加的實例構(gòu)造器<init>方法嘿般,一個是源碼中的drive()方法段标;
- 接下來2個字節(jié)是訪問標志,0x0001表示第一個方法是public炉奴;
- name_index值為0x0010逼庞,查找常量表可得方法名為“<init>”;
- descriptor_index值為0x0011瞻赶,對應常量值為“()V”往堡,所以我們可以反推出該方法的定義為:public void <init>()械荷,這其實是編譯器自動添加的一個實例構(gòu)造器方法,我們的源碼里并沒有該方法虑灰;
- 屬性表計數(shù)器為0x0001吨瞎,表示只有一個屬性,接下來就是該方法的第一個屬性表穆咐。第一個屬性表對應的屬性名稱索引值為0x0012颤诀,對應常量值為“Code”,說明此屬性是方法的字節(jié)碼描述对湃;
- 行文至此崖叫,方法的定義可以通過access_flags、name_index拍柒、descriptor_index來表達清楚心傀,但是方法里的代碼去哪里了呢?前面剛提到該方法有一個名為“code”的屬性拆讯,這個屬性就存儲了方法里的java代碼編譯后的字節(jié)碼指令脂男。
11.屬性表
在Class文件、字段表种呐、方法表中都可以攜帶自己的屬性表集合宰翅,以用于描述某些場景專有的信息。
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java代碼編譯成的字節(jié)碼指令 |
ConstantValue | 字段表 | final關(guān)鍵字定義的常量值 |
Deprecated | 類爽室、方法表汁讼、字段表 | 被聲明為deprecated的方法和字段 |
Exceptions | 方法表 | 方法拋出的異常 |
InnerClasses | 類文件 | 內(nèi)部類列表 |
LineNumberTable | Code屬性 | Java源碼的行號與字節(jié)碼指令的對應關(guān)系 |
LocalVariableTable | Code屬性 | 方法的局部變量描述 |
SourceFile | 類文件 | 源文件名稱 |
Synthetic | 類、方法表阔墩、字段表 | 標識方法或字段為編譯器自動生成的 |
不同的屬性有不同的數(shù)據(jù)結(jié)構(gòu)嘿架,在這里我們挑選2個最重要的屬性來講解一下:
11.1 Code屬性
Java方法里的代碼被編譯處理后,變?yōu)樽止?jié)碼指令存儲在方法表的Code屬性里啸箫,但并不是所有的方法表里都有Code屬性耸彪,例如接口或抽象類中的方法就可能沒有該屬性。
Code屬性數(shù)據(jù)結(jié)構(gòu):
類型 | 名稱 | 含義 |
---|---|---|
u2 | attribute_name_index | 屬性名稱索引 |
u4 | attribute_length | 屬性長度 |
u2 | max_stack | 操作數(shù)棧深度的最大值 |
u2 | max_locals | 局部變量表所需的存儲空間 |
u4 | code_length | 字節(jié)碼長度 |
u1 | code[code_length] | 存儲字節(jié)碼指令的一系列字節(jié)流 |
u2 | exception_table_length | 異常表長度 |
exception_info | exception_table | |
u2 | attributes_count | |
attribute_info | attributes[attributes_count] |
可以看到Code屬性數(shù)據(jù)結(jié)構(gòu)里還包含有其他屬性筐高,主要有LineNumberTable搜囱、LocalVariableTable。
LineNumberTable屬性數(shù)據(jù)結(jié)構(gòu)為:
LineNumberTable_attribute {
u2 attribute_name_index; //屬性名稱索引
u4 attribute_length; //屬性長度
u2 line_number_table_length;
{ u2 start_pc; //字節(jié)碼行號
u2 line_number; //java源碼行號
} line_number_table[line_number_table_length];
}
我們接著前面方法表的字節(jié)碼繼續(xù)分析柑土,前面分析到<init>方法只有一個名為Code的屬性值蜀肘,參照Code屬性的數(shù)據(jù)結(jié)構(gòu)我們可以看到如下圖所示:
11.2 ConstantValue屬性
只有當一個字段被聲明為static final時,并且該字段是基本數(shù)據(jù)類型或String類型時稽屏,編譯器才會在字段的屬性表集合中增加一個名為ConstantValue的屬性扮宠,所以ConstantValue屬性只會出現(xiàn)在字段表中,其數(shù)據(jù)結(jié)構(gòu)為:
類型 | 名稱 | 含義 |
---|---|---|
u2 | attribute_name_index | 屬性名稱索引 |
u2 | attribute_length | 屬性長度 |
u2 | constantvalue_index | 常量池常量的索引 |
12. 字段不同訪問標志的初始化賦值差異
我們先來看個例子,里面定義了3種不同訪問標志的字段:
public class Simple {
public static final String a = "abc";
public static String b = "abc";
public String c = "abc";
}
這個類里面定義了3個不同的變量坛增,都是賦值成“abc”获雕,但是虛擬機對它們的賦值時機是不同的:
- 對于非靜態(tài)(無static修飾)的字段,賦值會在實例構(gòu)造方法<init>()方法中進行收捣。
- 對于static修飾的字段:如果有final修飾并且該字段是基本數(shù)據(jù)類型或String類型届案,則會在該字段對應的字段表field_info中增加一個名為ConstantValue的屬性,賦值的時候使用該ConstantValue進行賦值罢艾;如果有final修飾楣颠,但該字段不是基本數(shù)據(jù)類型及String類型,則會在類構(gòu)造方法<clinit>
()中賦值咐蚯;如果沒有final修飾童漩,則會在類構(gòu)造方法<clinit>()中賦值。
訪問標志 | 數(shù)據(jù)類型 | 賦值策略 |
---|---|---|
static final | 基本數(shù)據(jù)類型或String | ConstantValue |
static final | 除基本數(shù)據(jù)和String以外 | <clinit>()方法 |
static | 任意類型 | <clinit>()方法 |
無static | 任意類型 | <init>()方法 |
采用javac Simple.java編譯該類之后春锋,同樣采用javap -v Simple.class查看class文件的字節(jié)碼信息矫膨,可以看到這3個字段的賦值方式:
java類加載機制系列文章: