玩命學JVM(一)—認識JVM和位元組碼文件
本篇文章的思維導圖
一、JVM的簡單介紹
1.1 JVM是什麼?
JVM (java virtual machine),java虛擬機,是一個虛構出來的計算機,但是有自己完善的硬件結構:處理器、堆棧、寄存器等。java虛擬機是用於執行位元組碼文件的。
1.2 JAVA為什麼能跨平台?
首先我們可以問一個這樣的問題,為什麼 C 語言不能跨平台?如下圖:
C語言在不同平台上的對應的編譯器會將其編譯為不同的機器碼文件,不同的機器碼文件只能在本平台中運行。
而java文件的執行過程如圖:
java通過javac將源文件編譯為.class文件(位元組碼文件),該位元組碼文件遵循了JVM的規範,使其可以在不同系統的JVM下運行。
小結
- java 代碼不是直接在計算機上執行的,而是在JVM中執行的,不同操作系統下的 JVM 不同,但是會提供相同的接口。
- javac 會先將 .java 文件編譯成二進制位元組碼文件,位元組碼文件與操作系統平台無關,只面向 JVM, 注意同一段代碼的位元組碼文件是相同的。
- 接着JVM執行位元組碼文件,不同操作系統下的JVM會將同樣的位元組碼文件映射為不同系統的API調用。
- JVM不是跨平台的,java是跨平台的。
1.3 JVM為什麼跨語言
前面提到”.class文件是一種遵循了JVM規範的位元組碼文件”,那麼不難想到,只要另一種語言也同樣了遵循了JVM規範,可將其源文件編譯為.class文件,就也能在 JVM 上運行。如下圖:
1.4 JDK、JRE、JVM的關係
我們看一下官方給的圖:
三者定義
- JDK:JDK(Java SE Development Kit),Java標準開發包,它提供了編譯、運行Java程序所需的各種工具和資源,包括Java編譯器(javac)、Java運行時環境(JRE),以及常用的Java類庫等。
- JRE:JRE( Java Runtime Environment) 、Java運行環境,用於解釋執行Java的位元組碼文件。普通用戶而只需要安裝 JRE 來運行 Java 程序。而程序開發者必須安裝JDK來編譯、調試程序。
- JVM:JVM(Java Virtual Mechinal),是JRE的一部分。負責解釋執行位元組碼文件,是可運行java位元組碼文件的虛擬計算機。
區別和聯繫
- JDK 用於開發,JRE 用於運行java程序 ;如果只是運行Java程序,可以只安裝JRE,無需安裝JDK。
- JDk包含JRE,JDK 和 JRE 中都包含 JVM。
- JVM 是 java 編程語言的核心並且具有平台獨立性。
二、位元組碼文件詳解
官方文檔地址://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.1
2.1 位元組碼文件的結構
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」等指的是每項數據的所佔的長度,u4表示佔4個位元組,u2表示佔2個位元組,以此類推。
.class
文件是以16進制組織的,一個16進制位可以用4個2進制位表示,一個2進制位是一個bit,所以一個16進制位是4個bit,兩個16進制位就是8bit = 1 byte。以Main.class
文件的開頭cafe
為例分析:
因此 u4 對應4個位元組,就是cafe babe
接下來先分析 ClassFile
的結構:
- magic
在 class 文件開頭的四個位元組, 存放着 class 文件的魔數, 這個魔數是 class 文件的標誌,是一個固定的值: 0xcafebabe 。 也就是說他是判斷一個文件是不是 class 格式的文件的標準, 如果開頭四個位元組不是 0xcafebabe , 那麼就說明它不是 class 文件, 不能被 JVM 識別。 - minor_version 和 major_version
次版本號和主版本號決定了該class file
文件的版本,如果 major_version 記作 M,minor_version 記作 m ,則該文件的版本號為:M.m。因此,可以按字典順序對類文件格式的版本進行排序,例如1.5 <2.0 <2.1。當且僅當v處於 Mi.0≤v≤Mj.m 的某個連續範圍內時,Java 虛擬機實現才能支持版本 v 的類文件格式。範圍列表如下:
- constant_pool_count
constant_pool_count 項的值等於 constant_pool 表中的條目數加1。如果 constant_pool 索引大於零且小於 constant_pool_count,則該索引被視為有效,但 CONSTANT_Long_info 和CONSTANT_Double_info 類型的常量除外。 - constant_pool
constant_pool 是一個結構表,表示各種字符串常量,類和接口名稱,字段名稱以及在ClassFile 結構及其子結構中引用的其他常量。 每個 constant_pool 表條目的格式由其第一個「標籤」位元組指示。constant_pool 表的索引從1到 constant_pool_count-1。
Java虛擬機指令不依賴於類,接口,類實例或數組的運行時布局。 相反,指令引用了constant_pool 表中的符號信息。
所有 constant_pool 表條目均具有以下常規格式:cp_info { u1 tag; u1 info[]; }
constant_pool 表中的每個條目都必須以一個1位元組的標籤開頭,該標籤指示該條目表示的常量的種類。 常量有17種,在下表中列出,並帶有相應的標記。每個標籤位元組後必須跟兩個或多個位元組,以提供有關特定常數的信息。 附加信息的格式取決於標籤位元組,即info數組的內容隨標籤的值而變化。
-
access_flags
access_flags 項的值是標誌的掩碼,用於表示對該類或接口的訪問權限和屬性。設置後,每個標誌的解釋在下表中指定。
-
this_class
this_class 項目的值必須是指向 constant_pool 表的有效索引。該索引處的 constant_pool 條目必須是代表此類文件定義的類或接口的 CONSTANT_Class_info 結構。CONSTANT_Class_info { u1 tag; u2 name_index; }
-
super_class
對於一個類,父類索引的值必須為零或必須是 constant_pool 表中的有效索引。 如果super_class 項的值非零,則該索引處的 constant_pool 條目必須是 CONSTANT_Class_info 結構,該結構表示此類文件定義的類的直接超類。 直接超類或其任何超類都不能在其 ClassFile結構的 access_flags 項中設置 ACC_FINAL 標誌。如果 super_class 項的值為零,則該類只可能是 java.lang.Object ,這是沒有直接超類的唯一類或接口。對於接口,父類索引的值必須始終是 constant_pool 表中的有效索引。該索引處的 constant_pool 條目必須是 java.lang.Object 的CONSTANT_Class_info 結構。 -
interfaces_count
interfaces_count 項目的值給出了此類或接口類型的直接超接口的數量。 -
interfaces[]
接口表的每個值都必須是 constant_pool 表中的有效索引。interfaces [i]的每個值(其中0≤i <interfaces_count)上的 constant_pool 條目必須是 CONSTANT_Class_info 結構,該結構描述當前類或接口類型的直接超接口。 -
fields_count
字段計數器的值給出了 fields 表中 field_info 結構的數量。 field_info 結構代表此類或接口類型聲明的所有字段,包括類變量和實例變量。 -
fields[]
字段表中的每個值都必須是field_info結構,以提供對該類或接口中字段的完整描述。 字段表僅包含此類或接口聲明的字段,不包含從超類或超接口繼承的字段。
字段結構如下:field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
-
methods_count
方法計數器的值表示方法表中 method_info 結構的數量。 -
methods[]
方法表中的每個值都必須是 method_info 結構,以提供對該類或接口中方法的完整描述。 如果在 method_info 結構的 access_flags 項中均未設置 ACC_NATIVE 和 ACC_ABSTRACT 標誌,則還將提供實現該方法的Java虛擬機指令;
method_info 結構表示此類或接口類型聲明的所有方法,包括實例方法,類方法,實例初始化方法以及任何類或接口初始化的方法。 方法表不包含表示從超類或超接口繼承的方法。
方法具有如下結構:method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
-
attributes_count
屬性計數器的值表示當前類的屬性表中的屬性數量。 -
attributes[]
注意,這裡的屬性並不是Java代碼裏面的類屬性(類字段),而是Java源文件便已有特有的一些屬性(不要與 fields 混淆),屬性的結構:
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個位元組:
cafe babe // 魔法數,標識為.class位元組碼文件
0000 0034 //版本號 52.0
001d //常量池長度 constant_pool_count 29-1=28
接着開始解析常量,先查看往後的第一個位元組:0a
,對應的常量類型CONSTANT_Methodref
,對應的結構為:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
tag佔一個位元組,class_index 佔2個位元組,name_and_type_index 佔2個自己,依次往後數,注意0a
就是tag,所以往後數2個位元組是 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 標識字符串
00 // 下標為0
06 // 6個位元組
3c 696e 6974 3e //<init>
01 //CONSTANT_Utf8 表示字符串
00 // 下標為0
03 // 3個位元組
2829 56 // ()v
01 //CONSTANT_Utf8 表示字符串
00 // 下標為0
04 // 4個位元組
436f 6465 // code
01 //CONSTANT_Utf8 表示字符串
00 // 下標為0
0f // 15個位元組
4c 696e 654e 756d 6265 7254 6162 6c65 //lineNumberTable
01 //CONSTANT_Utf8 表示字符串
00 // 下標為0
04 // 4個位元組
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
常量池往後的結構可繼續按照這種方式進行解析。現在我們採用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."<init>":()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 <init>
#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 // "<init>":()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."<init>":()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"
對比下可以發現與我們人工解析的結果是一致的。
小結
本文第一部分圍繞JVM的幾個常見的問題做了一些簡單介紹。第二部分詳細介紹了ClassFile的結構及 JVM 對 ClassFile 指定的規範(更多詳細的規範有興趣的讀者可查看官方文檔),接着按照規範進行了部分位元組碼的手動解析,並與 JVM 的解析結果進行了對比。個人認為作為偏應用層的programer沒必要去記憶這些「規範」,而是要跳出這些繁雜的規範掌握到以下幾點:
- 會藉助官方文檔對位元組碼文件做簡單閱讀。
- 理解位元組碼文件在整個執行過程的角色和作用,其實就是一個「編解碼」的過程。javac將.java文件按照JVM的規則生成位元組碼文件,JVM按照規範解析位元組碼文件為機器可執行的指令。
參考文獻:
//blog.csdn.net/peng_zhanxuan/article/details/104329859
//docs.oracle.com/javase/specs/jvms/se11/html/index.html
//blog.csdn.net/weelyy/article/details/78969412