一起學(xué)Java虛擬機(jī)系列:
前言
了解JVM是對Java程序員的基本要求,但是有多少同學(xué)和我有一樣醉心解bug堆布局,忘記了內(nèi)功修煉,對JVM的理解是零碎的。系統(tǒng)地學(xué)習(xí)一次JVM也許能讓我們在這條路走得更好更遠(yuǎn)。
無關(guān)性
平臺無關(guān)性
- “一次編寫,到處運(yùn)行(Write Once,Run Anywhere)”
- 各種不同平臺的Java虛擬機(jī),以及所有平臺都統(tǒng)一支持的程序存儲格式——字節(jié)碼(Byte Code)
語言無關(guān)性
- Java虛擬機(jī)不與包括Java語言在內(nèi)的任何程序語言綁定,它只與“Class文件”這種特定的二進(jìn)制文件格式所關(guān)聯(lián),Class文件中包含了Java虛擬機(jī)指令集、符號表以及若干其他輔助信息。
- Java的規(guī)范拆分成了《Java語言規(guī)范》(The Java Language Specification)及《Java虛擬機(jī)規(guī)范(The Java Virtual Machine Specification)兩部分
- 作為一個通用的、與機(jī)器無關(guān)的執(zhí)行平臺,任何其他語言的實(shí)現(xiàn)者都可以將Java虛擬機(jī)作為他們語言的運(yùn)行基礎(chǔ),以Class文件作為他們產(chǎn)品的交付媒介。
Class類文件的結(jié)構(gòu)
Class文件是一組以8個字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在文
件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內(nèi)容幾乎全部是程序運(yùn)行的必要數(shù)
據(jù),沒有空隙存在。當(dāng)遇到需要占用8個字節(jié)以上空間的數(shù)據(jù)項(xiàng)時,則會按照高位在前(大端表示法(big-endian))的方式分割成若干個8個字節(jié)進(jìn)行存儲。
“無符號數(shù)”和"表"
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Class文件格式采用一種類似于C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲數(shù)
據(jù),這種偽結(jié)構(gòu)中只有兩種數(shù)據(jù)類型:“無符號數(shù)”和“表”。
- 無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1、u2、u4、u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值。
- 表是由多個無符號數(shù)或者其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型,為了便于區(qū)分,所有表的命名都習(xí)慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個Class文件本質(zhì)上也可以視作是一張表。
使用010Editor查看class文件結(jié)構(gòu)
通過010Editor查看HelloWorld.class
public class HelloWorld {
private static final String HELLO_WORLD = "Hello World!";
public static void main(String args[]) {
System.out.println(HELLO_WORLD);
}
}
下載地址:
https://www.sweetscape.com/download/010editor/
下載完成后打開class文件會自動提示安裝CLASSAdv.bt插件
魔數(shù)
- magic
每個Class文件的頭4個字節(jié)被稱為魔數(shù)(Magic Number),它的唯一作用是確定這個文件是否為
一個能被虛擬機(jī)接受的Class文件,Class文件的魔數(shù)取得很有“浪漫氣息”,值為0xCAFEBABE
版本號
- minorersion 和 major_version
緊接著魔數(shù)的4個字節(jié)存儲的是Class文件的版本號:第5和第6個字節(jié)是次版本號(Minor Version),第7和第8個字節(jié)是主版本號(Major Version)
每個版本的 JDK 都有自己特定的版本號。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能運(yùn)行高版本的 Class 文件,即使文件格式?jīng)]有發(fā)生任何變化,虛擬機(jī)也拒絕執(zhí)行高于其版本號的 Class 文件
常量池
- u2 constant_pool_count
由于常量池中常量的數(shù)量是不固定的,所以在常量池的入口需要放置一項(xiàng)u2類型的數(shù)據(jù),代表常
量池容量計數(shù)值(constant_pool_count)
需要注意的是,常量池的下標(biāo)是從
1 開始的,也就代表該 Class 文件具有 36 個常量。那么,為什么下標(biāo)要從 1 開始呢?目的是為了表示在特定情況下 不引用任何一個常量池項(xiàng),這時候下標(biāo)就用 0 表示。
- u2 constant_pool
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References),常量池中每一項(xiàng)常量都是一個表。
常量池的數(shù)據(jù)類型有十幾種,各自都有自己的數(shù)據(jù)結(jié)構(gòu),但是他們都有一個共有屬性 tag。tag 是標(biāo)志位,標(biāo)記是哪一種數(shù)據(jù)結(jié)構(gòu)。
常見常量池類型:
| 類 型 | 標(biāo) 志 | 描 述 |
|---|---|---|
| CONSTANT_Utf8_info | 1 | UTF-8 編碼的字符串 |
| CONSTANT_Integer_info | 3 | 整型字面量 |
| CONSTANT_Float_info | 4 | 浮點(diǎn)型字面量 |
| COSTANT_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_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
| CONSTANT_NameAndType_info | 12 | 字段或方法的部分符號引用 |
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
| CONSTANT_MethodType_info | 16 | 標(biāo)識方法類型 |
| CONSTANT_InvokeDynamic_info | 18 | 表示一個動態(tài)方法調(diào)用點(diǎn) |
| CONSTANT_Module_ino | 19 | 表示一個模塊 |
| CONSTANT_Package_ino | 20 | 表示一個模塊開或者導(dǎo)出的包 |
我們再來粗略解析一下常量池的第一項(xiàng)常量
- tag :10 代表類型CONSTANT_Methodref_info,類中方法的符號引用
- classIndex : 6 這個是一個常量池索引, 代表第5個數(shù)據(jù)項(xiàng)(CONSTANT_Methodref_info 的 class_index 指向的數(shù)據(jù)項(xiàng)永遠(yuǎn)是 CONSTANT_Class_info)
- name_and_type_index :23 同樣是一個常量池索引
先來看:classIndex:
- tag :7代表CONSTANT_Class_info
- name_index :30 又是一個常量池索引
- 數(shù)據(jù)項(xiàng)29代表了一個CONSTANT_Utf8_info類型,bytes[16]里是字符串的內(nèi)容,從 010Editor 解析內(nèi)容可以看到這個字符串是 java/lang/System,表示類的權(quán)限定名
回頭在來看第一個常量的 name_and_type_index
- tag : 12 代表CONSTANT_NameAndType_info
- name_index 表示字段或者方法的非限定名,這里的值是 <init>
- descriptor_index表示字段描述符或者方法描述符,這里的值是 ()V。
這樣,常量池的第一個數(shù)據(jù)項(xiàng)就分析完了,后面的每一個數(shù)據(jù)項(xiàng)都可以按照這樣分析
javap工具
剩余部分全部手動分析太雷,我們偷個懶。在JDK的bin目錄中,Oracle公司已經(jīng)為我們
準(zhǔn)備好一個專門用于分析Class文件字節(jié)碼的工具:javap
HP-ProDesk-680-G6-PCI-Microtower-PC:~/DEBUG$ javap -verbose HelloWorld.class
Classfile /home/mi/DEBUG/HelloWorld.class
Last modified May 12, 2021; size 641 bytes
MD5 checksum 1910a4531e5743c190636067d43d4bc4
Compiled from "HelloWorld.java"
public class com.wang.javavmdemo.HelloWorld
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/wang/javavmdemo/HelloWorld
super_class: #6 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #26 // com/wang/javavmdemo/HelloWorld
#4 = String #27 // Hello World!
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #30 // java/lang/Object
#7 = Utf8 HELLO_WORLD
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 ConstantValue
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/wang/javavmdemo/HelloWorld;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 SourceFile
#22 = Utf8 HelloWorld.java
#23 = NameAndType #10:#11 // "<init>":()V
#24 = Class #31 // java/lang/System
#25 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#26 = Utf8 com/wang/javavmdemo/HelloWorld
#27 = Utf8 Hello World!
#28 = Class #34 // java/io/PrintStream
#29 = NameAndType #35:#36 // println:(Ljava/lang/String;)V
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/String;)V
{
public com.wang.javavmdemo.HelloWorld();
descriptor: ()V
flags: (0x0001) 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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wang/javavmdemo/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello World!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature我
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
類文件的剩余部分,先預(yù)覽一下:
訪問標(biāo)志
- access_flags
在常量池結(jié)束之后,緊接著的2個字節(jié)代表訪問標(biāo)志(access_flags),這個標(biāo)志用于識別一些類或
者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract
類型;如果是類的話,是否被聲明為final;等等
標(biāo)志位及含義表:
| 標(biāo)志名稱 | 標(biāo) 志 值 | 含 義 |
|---|---|---|
| ACC_PUBIC | 0x0001 | 是否為 public 類型 |
| ACC_FINAL | 0x0010 | 是否聲明為 final |
| ACC_SUPER | 0x0020 | JDK1.0.2 之后編譯出來的類這個標(biāo)志都必須為真 |
| ACC_INTERFACE | 0x0200 | 是否為接口 |
| ACC_ABSTRACT | 0x0400 | 是否為 abstract 類型 |
| ACC_SYNTHETIC | 0x1000 | 標(biāo)記這個類并非由用戶代碼產(chǎn)生 |
| ACC_ANNOTATION | 0x2000 | 是否為注解 |
| ACC_ENUM | 0x4000 | 是否為枚舉類型 |
| ACC_MODULE | 0x8000 | 是否為模塊 |
HelloWorld是一個普通類,不是接口、注解、枚舉類型或者莫模塊,并且被public關(guān)鍵字修飾
因此他的access_flag應(yīng)該為ACC_PUBIC|ACC_SUPER,轉(zhuǎn)換為10進(jìn)制就是33
類索引、父類索引與接口索引集合
Class文件中由這三項(xiàng)數(shù)據(jù)來確定該類型的繼承關(guān)系。
- this_class類索引用于確定這個類的全限定名。value指向常量池中的索引。
- super_class父類索引用于確定這個類的父類的全限定名。由于Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java類的父類索引都不為0。
- interfaces_count,表示的是該類實(shí)現(xiàn)的接口數(shù)量。HelloWorld未實(shí)現(xiàn)任何接口所以是0。
- 如果實(shí)現(xiàn)了若干接口,這些接口信息將存儲在之后的 interfaces[] 之中。接口索引集合就用來描述這個類實(shí)現(xiàn)了哪些接口,這些被實(shí)現(xiàn)的接口將按implements關(guān)鍵字(如果這個Class文件表示的是一個接口,則應(yīng)當(dāng)是extends關(guān)鍵字)后的接口順序從左到右排列在接口索引集合中。
字段表長度和字段表集合
Java語言中的“字段”(Field)包括類級變量以及實(shí)例級變量,但不包括在方法內(nèi)部聲明的局部變量。
- fileds_count 表示該類中聲明的變量個數(shù)
- filed_info 表示該類中聲明的變量信息
private static final String HELLOWORLD = "HelloWorld";
可以看出來,Java中描述一個字段首先是訪問范圍,是公有的還是私有的,或者受保護(hù)的,這個信息決定了字段是否堆特定范圍的類可見。
其次是一些關(guān)鍵字修飾的描述信息,是實(shí)例變量還是類變量,是否可變,并發(fā)可見性,是否可被序列化等,這些關(guān)鍵字包括static、final 、volatile、transient等。
在后面便是字段的數(shù)據(jù)類型(基本數(shù)據(jù)類型、數(shù)組、對象)和名稱。
上述的這些修飾符都是用布爾值來描述的,而數(shù)據(jù)類型和名稱都是不確定的,通常引用常量池的常量來描述。
方法表長度和方法表集合
Class文件存儲
格式中對方法的描述與對字段的描述采用了幾乎完全一致的方式,方法表的結(jié)構(gòu)如同字段表一樣,依
次包括訪問標(biāo)志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表
集合(attributes)幾項(xiàng)
方法的定義可以通過訪問標(biāo)志、名稱和描述符索引來表述清楚,那么方法中的代碼又在哪里呢?
方法里的java代碼經(jīng)過編譯器編譯成字節(jié)碼指令后,存放在方法的屬性表集合里一個名為“Code”的屬性里。
attribute_name_index 對應(yīng)一個類型為CONSTANT_Utf8_info的常量索引,常量值固定為“Code”
屬性表長度和屬性表集合
Class文件、字段表、方法表都可以攜帶自己的屬性表集合,以描述某些場景專有的信息
與class文件其它數(shù)據(jù)項(xiàng)目嚴(yán)格要求順序長度不同,屬性表集合限制相對比較寬松,不要求各個屬性表具有嚴(yán)格順序,只要不與已有屬性名重復(fù),任何人實(shí)現(xiàn)的編譯器均可向?qū)傩员韺懭胱约旱膶傩?,jvm運(yùn)行時會自動忽略掉不認(rèn)識的屬性。
java7中定義的屬性如下表:
| 屬性名稱 | 使用位置 | 含義 |
|---|---|---|
| Code | 方法表 | Java代碼編譯成的字節(jié)碼指令 |
| ConstantValue | 字段表 | final關(guān)鍵字定義的常量池 |
| Deprecated | 類,方法,字段表 | 被聲明為deprecated的方法和字段 |
| Exceptions | 方法表 | 方法拋出的異常 |
| EnclosingMethod | 類文件 | 僅當(dāng)一個類為局部類或者匿名類是才能擁有這個屬性,這個屬性用于標(biāo)識這個類所在的外圍方法 |
| InnerClass | 類文件 | 內(nèi)部類列表 |
| LineNumberTable | Code屬性 | Java源碼的行號與字節(jié)碼指令的對應(yīng)關(guān)系 |
| LocalVariableTable | Code屬性 | 方法的局部便狼描述 |
| StackMapTable | Code屬性 | JDK1.6中新增的屬性,供新的類型檢查檢驗(yàn)器檢查和處理目標(biāo)方法的局部變量和操作數(shù)有所需要的類是否匹配 |
| Signature | 類,方法表,字段表 | 用于支持泛型情況下的方法簽名 |
| SourceFile | 類文件 | 記錄源文件名稱 |
| SourceDebugExtension | 類文件 | 用于存儲額外的調(diào)試信息 |
| Synthetic | 類,方法表,字段表 | 標(biāo)志方法或字段為編譯器自動生成的 |
| LocalVariableTypeTable | 類 | 使用特征簽名代替描述符,是為了引入泛型語法之后能描述泛型參數(shù)化類型而添加 |
| RuntimeVisibleAnnotations | 類,方法表,字段表 | 為動態(tài)注解提供支持 |
| RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是運(yùn)行時不可見的 |
| RuntimeVisibleParameterAnnotation | 方法表 | 作用與RuntimeVisibleAnnotations屬性類似,只不過作用對象為方法 |
| RuntimeInvisibleParameterAnnotation | 方法表 | 作用與RuntimeInvisibleAnnotations屬性類似,作用對象哪個為方法參數(shù) |
| AnnotationDefault | 方法表 | 用于記錄注解類元素的默認(rèn)值 |
| BootstrapMethods | 類文件 | 用于保存invokeddynamic指令引用的引導(dǎo)方式限定符 |
關(guān)于字段表集合、屬性表集合、方法表集合的結(jié)構(gòu) 以及acess_flag的列表
可以像查字典一樣查閱《深入理解JAVA虛擬機(jī)》6.3小節(jié)的內(nèi)容。我們只需要理解原理。