玩命學(xué)JVM:認(rèn)識JVM和字節(jié)碼文件

點擊上方「藍(lán)字」關(guān)注我們

source:https://www.cnblogs.com/cleverziv/p/13751488.html
本篇文章的思維導(dǎo)圖
一、JVM的簡單介紹
1.1 JVM是什么?
JVM (java virtual machine),java虛擬機(jī),是一個虛構(gòu)出來的計算機(jī),但是有自己完善的硬件結(jié)構(gòu):處理器、堆棧、寄存器等。java虛擬機(jī)是用于執(zhí)行字節(jié)碼文件的。
1.2 JAVA為什么能跨平臺?
首先我們可以問一個這樣的問題,為什么 C 語言不能跨平臺?如下圖:
C語言在不同平臺上的對應(yīng)的編譯器會將其編譯為不同的機(jī)器碼文件,不同的機(jī)器碼文件只能在本平臺中運行。
而java文件的執(zhí)行過程如圖:
java通過javac將源文件編譯為.class文件(字節(jié)碼文件),該字節(jié)碼文件遵循了JVM的規(guī)范,使其可以在不同系統(tǒng)的JVM下運行。
小結(jié)
java 代碼不是直接在計算機(jī)上執(zhí)行的,而是在JVM中執(zhí)行的,不同操作系統(tǒng)下的 JVM 不同,但是會提供相同的接口。
javac 會先將 .java 文件編譯成二進(jìn)制字節(jié)碼文件,字節(jié)碼文件與操作系統(tǒng)平臺無關(guān),只面向 JVM, 注意同一段代碼的字節(jié)碼文件是相同的。
接著JVM執(zhí)行字節(jié)碼文件,不同操作系統(tǒng)下的JVM會將同樣的字節(jié)碼文件映射為不同系統(tǒng)的API調(diào)用。
JVM不是跨平臺的,java是跨平臺的。
1.3 JVM為什么跨語言
前面提到".class文件是一種遵循了JVM規(guī)范的字節(jié)碼文件",那么不難想到,只要另一種語言也同樣了遵循了JVM規(guī)范,可將其源文件編譯為.class文件,就也能在 JVM 上運行。如下圖:
1.4 JDK、JRE、JVM的關(guān)系
我們看一下官方給的圖:
1.4.1 三者定義
JDK:JDK(Java SE Development Kit),Java標(biāo)準(zhǔn)開發(fā)包,它提供了編譯、運行Java程序所需的各種工具和資源,包括Java編譯器(javac)、Java運行時環(huán)境(JRE),以及常用的Java類庫等。
JRE:JRE( Java Runtime Environment) 、Java運行環(huán)境,用于解釋執(zhí)行Java的字節(jié)碼文件。普通用戶而只需要安裝 JRE 來運行 Java 程序。而程序開發(fā)者必須安裝JDK來編譯、調(diào)試程序。
JVM:JVM(Java Virtual Mechinal),是JRE的一部分。負(fù)責(zé)解釋執(zhí)行字節(jié)碼文件,是可運行java字節(jié)碼文件的虛擬計算機(jī)。
1.4.2 區(qū)別和聯(lián)系
JDK 用于開發(fā),JRE 用于運行java程序 ;如果只是運行Java程序,可以只安裝JRE,無需安裝JDK。
JDk包含JRE,JDK 和 JRE 中都包含 JVM。
JVM 是 java 編程語言的核心并且具有平臺獨立性。
二、字節(jié)碼文件詳解
官方文檔地址:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.1
2.1 字節(jié)碼文件的結(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];
}
"ClassFile"中的“u4、u2”等指的是每項數(shù)據(jù)的所占的長度,u4表示占4個字節(jié),u2表示占2個字節(jié),以此類推。
.class文件是以16進(jìn)制組織的,一個16進(jìn)制位可以用4個2進(jìn)制位表示,一個2進(jìn)制位是一個bit,所以一個16進(jìn)制位是4個bit,兩個16進(jìn)制位就是8bit = 1 byte。以Main.class文件的開頭cafe為例分析:
因此 u4 對應(yīng)4個字節(jié),就是?cafe babe
接下來先分析?ClassFile的結(jié)構(gòu):
magic
在 class 文件開頭的四個字節(jié), 存放著 class 文件的魔數(shù), 這個魔數(shù)是 class 文件的標(biāo)志,是一個固定的值:0xcafebabe 。也就是說他是判斷一個文件是不是 class 格式的文件的標(biāo)準(zhǔn), 如果開頭四個字節(jié)不是 0xcafebabe , 那么就說明它不是 class 文件, 不能被 JVM 識別。minor_version 和 major_version
次版本號和主版本號決定了該class file文件的版本,如果 major_version 記作 M,minor_version 記作 m ,則該文件的版本號為:M.m。因此,可以按字典順序?qū)︻愇募袷降陌姹具M(jìn)行排序,例如1.5 <2.0 <2.1。當(dāng)且僅當(dāng)v處于 Mi.0≤v≤Mj.m 的某個連續(xù)范圍內(nèi)時,Java 虛擬機(jī)實現(xiàn)才能支持版本 v 的類文件格式。范圍列表如下:constant_pool_count
constant_pool_count 項的值等于 constant_pool 表中的條目數(shù)加1。如果 constant_pool 索引大于零且小于 constant_pool_count,則該索引被視為有效,但 CONSTANT_Long_info 和CONSTANT_Double_info 類型的常量除外。constant_pool
constant_pool 是一個結(jié)構(gòu)表,表示各種字符串常量,類和接口名稱,字段名稱以及在ClassFile 結(jié)構(gòu)及其子結(jié)構(gòu)中引用的其他常量。每個 constant_pool 表條目的格式由其第一個“標(biāo)簽”字節(jié)指示。constant_pool 表的索引從1到 constant_pool_count-1。
Java虛擬機(jī)指令不依賴于類,接口,類實例或數(shù)組的運行時布局。相反,指令引用了constant_pool 表中的符號信息。
所有 constant_pool 表條目均具有以下常規(guī)格式:cp_info {
u1 tag;
u1 info[];
}
constant_pool 表中的每個條目都必須以一個1字節(jié)的標(biāo)簽開頭,該標(biāo)簽指示該條目表示的常量的種類。常量有17種,在下表中列出,并帶有相應(yīng)的標(biāo)記。每個標(biāo)簽字節(jié)后必須跟兩個或多個字節(jié),以提供有關(guān)特定常數(shù)的信息。附加信息的格式取決于標(biāo)簽字節(jié),即info數(shù)組的內(nèi)容隨標(biāo)簽的值而變化。
access_flags
access_flags 項的值是標(biāo)志的掩碼,用于表示對該類或接口的訪問權(quán)限和屬性。設(shè)置后,每個標(biāo)志的解釋在下表中指定。this_class
this_class 項目的值必須是指向 constant_pool 表的有效索引。該索引處的 constant_pool 條目必須是代表此類文件定義的類或接口的 CONSTANT_Class_info 結(jié)構(gòu)。CONSTANT_Class_info {
u1 tag;
u2 name_index;
}super_class
對于一個類,父類索引的值必須為零或必須是 constant_pool 表中的有效索引。如果super_class 項的值非零,則該索引處的 constant_pool 條目必須是 CONSTANT_Class_info 結(jié)構(gòu),該結(jié)構(gòu)表示此類文件定義的類的直接超類。直接超類或其任何超類都不能在其 ClassFile結(jié)構(gòu)的 access_flags 項中設(shè)置 ACC_FINAL 標(biāo)志。如果 super_class 項的值為零,則該類只可能是 java.lang.Object ,這是沒有直接超類的唯一類或接口。對于接口,父類索引的值必須始終是 constant_pool 表中的有效索引。該索引處的 constant_pool 條目必須是 java.lang.Object 的CONSTANT_Class_info 結(jié)構(gòu)。interfaces_count
interfaces_count 項目的值給出了此類或接口類型的直接超接口的數(shù)量。interfaces[]
接口表的每個值都必須是 constant_pool 表中的有效索引。interfaces [i]的每個值(其中0≤ifields_count
字段計數(shù)器的值給出了 fields 表中 field_info 結(jié)構(gòu)的數(shù)量。field_info 結(jié)構(gòu)代表此類或接口類型聲明的所有字段,包括類變量和實例變量。fields[]
字段表中的每個值都必須是field_info結(jié)構(gòu),以提供對該類或接口中字段的完整描述。字段表僅包含此類或接口聲明的字段,不包含從超類或超接口繼承的字段。
字段結(jié)構(gòu)如下:field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}methods_count
方法計數(shù)器的值表示方法表中 method_info 結(jié)構(gòu)的數(shù)量。methods[]
方法表中的每個值都必須是 method_info 結(jié)構(gòu),以提供對該類或接口中方法的完整描述。如果在 method_info 結(jié)構(gòu)的 access_flags 項中均未設(shè)置 ACC_NATIVE 和 ACC_ABSTRACT 標(biāo)志,則還將提供實現(xiàn)該方法的Java虛擬機(jī)指令;
method_info 結(jié)構(gòu)表示此類或接口類型聲明的所有方法,包括實例方法,類方法,實例初始化方法以及任何類或接口初始化的方法。方法表不包含表示從超類或超接口繼承的方法。
方法具有如下結(jié)構(gòu):method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}attributes_count
屬性計數(shù)器的值表示當(dāng)前類的屬性表中的屬性數(shù)量。attributes[]
注意,這里的屬性并不是Java代碼里面的類屬性(類字段),而是Java源文件便已有特有的一些屬性(不要與 fields 混淆),屬性的結(jié)構(gòu):xml attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
屬性列表:
2.2 實例分析
首先寫一段Java程序,我們熟悉的“Hello World”
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
使用javac Main.java編譯生成Main.class文件:
cafe?babe?0000?0034?001d?0a00?0600?0f09
0010?0011?0800?120a?0013?0014?0700?1507
0016?0100?063c?696e?6974?3e01?0003?2829
5601?0004?436f?6465?0100?0f4c?696e?654e
756d?6265?7254?6162?6c65?0100?046d?6169
6e01?0016?285b?4c6a?6176?612f?6c61?6e67
2f53?7472?696e?673b?2956?0100?0a53?6f75
7263?6546?696c?6501?0009?4d61?696e?2e6a
6176?610c?0007?0008?0700?170c?0018?0019
0100?0b48?656c?6c6f?2057?6f72?6c64?0700
1a0c?001b?001c?0100?044d?6169?6e01?0010
6a61?7661?2f6c?616e?672f?4f62?6a65?6374
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?0013?6a61?7661?2f69?6f2f?5072?696e
7453?7472?6561?6d01?0007?7072?696e?746c
6e01?0015?284c?6a61?7661?2f6c?616e?672f
5374?7269?6e67?3b29?5600?2100?0500?0600
0000?0000?0200?0100?0700?0800?0100?0900
0000?1d00?0100?0100?0000?052a?b700?01b1
0000?0001?000a?0000?0006?0001?0000?0001
0009?000b?000c?0001?0009?0000?0025?0002
0001?0000?0009?b200?0212?03b6?0004?b100
0000?0100?0a00?0000?0a00?0200?0000?0400
0800?0500?0100?0d00?0000?0200?0e開始按照以上知識破譯上面的Main.class文件
按順序解析,首先是前10個字節(jié):
cafe babe // 魔法數(shù),標(biāo)識為.class字節(jié)碼文件
0000 0034 //版本號 52.0
001d //常量池長度 constant_pool_count 29-1=28
接著開始解析常量,先查看往后的第一個字節(jié):0a,對應(yīng)的常量類型CONSTANT_Methodref,對應(yīng)的結(jié)構(gòu)為:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
tag占一個字節(jié),class_index 占2個字節(jié),name_and_type_index 占2個自己,依次往后數(shù),注意0a就是tag,所以往后數(shù)2個字節(jié)是 class_index
00 06 // class_index 指向常量池中第6個常量所代表的類
00 0f // name_and_type_index 指向常量池中第15個常量所代表的方法
通過以上方法逐個解析,最終可得到常量池為:
0a?//?10?CONSTANT_Methodref
00?06?//?指向常量池中第6個常量所代表的類
00?0f?//?指向常量池中第15個常量所代表的方法
09?CONSTANT_Fieldref
0010?//?指向常量池中第16個常量所代表的類
0011?//?指向常量池中第17個常量所代表的變量
08?//?CONSTANT_String
00?12?//?指向常量池中第18個常量所代表的變量
0a?//?CONSTANT_Methodref
0013?//?指向常量池中第19個常量所代表的類
0014?//?指向常量池中第20個常量所代表的方法
07?//?CONSTANT_Class
00?15?//?指向常量池中第21個常量所代表的變量
07?//?CONSTANT_Class
0016?//?指向常量池中第22個常量所代表的變量
01?//?CONSTANT_Utf8?標(biāo)識字符串
00?//?下標(biāo)為0
06?//?6個字節(jié)
3c?696e?6974?3e?//
01?//CONSTANT_Utf8?表示字符串
00?//?下標(biāo)為0
03?//?3個字節(jié)
2829?56?//?()v
01?//CONSTANT_Utf8?表示字符串
00?//?下標(biāo)為0
04?//?4個字節(jié)
436f?6465?//?code
01?//CONSTANT_Utf8?表示字符串
00?//?下標(biāo)為0
0f?//?15個字節(jié)
4c?696e?654e?756d?6265?7254?6162?6c65?//lineNumberTable
01?//CONSTANT_Utf8?表示字符串
00?//?下標(biāo)為0
04?//?4個字節(jié)
6d?6169?6e?//main
01?
00
16?
285b?4c6a?6176?612f?6c61?6e67?2f53?7472?696e?673b?2956?//([Ljava/lang/String;)V
0100
0a?//10
53?6f75?7263?6546?696c?65?//sourceFile
01?00
09?
4d61?696e?2e6a?6176?61?//Main.java
0c?//?CONSTANT_NameAndType
0007?//nameIndex:7
0008?//descriptor_index:8
07?//CONSTANT_Class
00?17?//?第21個變量
0c
0018?
0019
0100
0b
48?656c?6c6f?2057?6f72?6c64?//?Hello?World
07
00?1a
0c?001b?001c
0100?
04
4d?6169?6e?//main
01?00
10
6a61?7661?2f6c?616e?672f?4f62?6a65?6374?//java/lang/Object
0100?
10
6a?6176?612f?6c61?6e67?2f53?7973?7465?6d?//?java/lang/System
01?00
03?
6f75?74?//?out
01?00
15?
4c6a?6176?612f?696f?2f50?7269?6e74?5374?7265?616d?3b?//Ljava/io/PrintStream;
01?00
13?
6a61?7661?2f69?6f2f?5072?696e?7453?7472?6561?6d?//?java/io/PrintStrea
01?00
07?
7072?696e?746c?6e?//println
01?00
15?
284c?6a61?7661?2f6c?616e?672f?5374?7269?6e67?3b29?56?//?(ljava/lang/String/String;)V常量池往后的結(jié)構(gòu)可繼續(xù)按照這種方式進(jìn)行解析。現(xiàn)在我們采用java自帶的方法來將.class文件反編譯,并驗證我們以上的解析是正確的。
使用javap -v Main.class可得到:
?Last?modified?2020-9-29;?size?413?bytes
??MD5?checksum?8b2b7cdf6c4121be8e242746b4dea946
??Compiled?from?"Main.java"
public?class?Main
??minor?version:?0
??major?version:?52
??flags:?ACC_PUBLIC,?ACC_SUPER
Constant?pool:
???#1?=?Methodref??????????#6.#15?????????//?java/lang/Object."" :()V
???#2?=?Fieldref???????????#16.#17????????//?java/lang/System.out:Ljava/io/PrintStream;
???#3?=?String?????????????#18????????????//?Hello?World
???#4?=?Methodref??????????#19.#20????????//?java/io/PrintStream.println:(Ljava/lang/String;)V
???#5?=?Class??????????????#21????????????//?Main
???#6?=?Class??????????????#22????????????//?java/lang/Object
???#7?=?Utf8???????????????
???#8?=?Utf8???????????????()V
???#9?=?Utf8???????????????Code
??#10?=?Utf8???????????????LineNumberTable
??#11?=?Utf8???????????????main
??#12?=?Utf8???????????????([Ljava/lang/String;)V
??#13?=?Utf8???????????????SourceFile
??#14?=?Utf8???????????????Main.java
??#15?=?NameAndType????????#7:#8??????????//?"" :()V
??#16?=?Class??????????????#23????????????//?java/lang/System
??#17?=?NameAndType????????#24:#25????????//?out:Ljava/io/PrintStream;
??#18?=?Utf8???????????????Hello?World
??#19?=?Class??????????????#26????????????//?java/io/PrintStream
??#20?=?NameAndType????????#27:#28????????//?println:(Ljava/lang/String;)V
??#21?=?Utf8???????????????Main
??#22?=?Utf8???????????????java/lang/Object
??#23?=?Utf8???????????????java/lang/System
??#24?=?Utf8???????????????out
??#25?=?Utf8???????????????Ljava/io/PrintStream;
??#26?=?Utf8???????????????java/io/PrintStream
??#27?=?Utf8???????????????println
??#28?=?Utf8???????????????(Ljava/lang/String;)V
{
??public?Main();
????descriptor:?()V
????flags:?ACC_PUBLIC
????Code:
??????stack=1,?locals=1,?args_size=1
?????????0:?aload_0
?????????1:?invokespecial?#1??????????????????//?Method?java/lang/Object."" :()V
?????????4:?return
??????LineNumberTable:
????????line?1:?0
??public?static?void?main(java.lang.String[]);
????descriptor:?([Ljava/lang/String;)V
????flags:?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???????????#3??????????????????//?String?Hello?World
?????????5:?invokevirtual?#4??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V
?????????8:?return
??????LineNumberTable:
????????line?4:?0
????????line?5:?8
}
SourceFile:?"Main.java"
對比下可以發(fā)現(xiàn)與我們?nèi)斯そ馕龅慕Y(jié)果是一致的。
小結(jié)
本文第一部分圍繞JVM的幾個常見的問題做了一些簡單介紹。第二部分詳細(xì)介紹了ClassFile的結(jié)構(gòu)及 JVM 對 ClassFile 指定的規(guī)范(更多詳細(xì)的規(guī)范有興趣的讀者可查看官方文檔),接著按照規(guī)范進(jìn)行了部分字節(jié)碼的手動解析,并與 JVM 的解析結(jié)果進(jìn)行了對比。個人認(rèn)為作為偏應(yīng)用層的programer沒必要去記憶這些“規(guī)范”,而是要跳出這些繁雜的規(guī)范掌握到以下幾點:
會借助官方文檔對字節(jié)碼文件做簡單閱讀。
理解字節(jié)碼文件在整個執(zhí)行過程的角色和作用,其實就是一個“編解碼”的過程。javac將.java文件按照J(rèn)VM的規(guī)則生成字節(jié)碼文件,JVM按照規(guī)范解析字節(jié)碼文件為機(jī)器可執(zhí)行的指令。

掃碼二維碼
獲取更多精彩
Java樂園











