玩命學JVM(一)—認識JVM和位元組碼檔案
阿新 • • 發佈:2020-09-29
**本篇文章的思維導圖**
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/JVM.png)
## 一、JVM的簡單介紹
###1.1 JVM是什麼?
JVM (java virtual machine),java虛擬機器,是一個虛構出來的計算機,但是有自己完善的硬體結構:處理器、堆疊、暫存器等。java虛擬機器是用於執行位元組碼檔案的。
###1.2 JAVA為什麼能跨平臺?
首先我們可以問一個這樣的問題,為什麼 C 語言不能跨平臺?如下圖:
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/C%E8%AF%AD%E8%A8%80%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.png)
C語言在不同平臺上的對應的編譯器會將其編譯為不同的機器碼檔案,不同的機器碼檔案只能在本平臺中執行。
而java檔案的執行過程如圖:
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/java%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png)
java通過javac將原始檔編譯為.class檔案(位元組碼檔案),該位元組碼檔案遵循了JVM的規範,使其可以在不同系統的JVM下執行。
**小結**
- java 程式碼不是直接在計算機上執行的,而是在JVM中執行的,不同作業系統下的 JVM 不同,但是會提供相同的介面。
- javac 會先將 .java 檔案編譯成二進位制位元組碼檔案,位元組碼檔案與作業系統平臺無關,只面向 JVM, 注意同一段程式碼的位元組碼檔案是相同的。
- 接著JVM執行位元組碼檔案,不同作業系統下的JVM會將同樣的位元組碼檔案對映為不同系統的API呼叫。
- JVM不是跨平臺的,java是跨平臺的。
###1.3 JVM為什麼跨語言
前面提到".class檔案是一種遵循了JVM規範的位元組碼檔案",那麼不難想到,只要另一種語言也同樣了遵循了JVM規範,可將其原始檔編譯為.class檔案,就也能在 JVM 上執行。如下圖:
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/JVM%E8%B7%A8%E8%AF%AD%E8%A8%80.png)
###1.4 JDK、JRE、JVM的關係
我們看一下官方給的圖:
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/JDK%E5%85%A8%E5%9B%BE.png)
####三者定義
- **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位元組碼檔案的虛擬計算機。
####區別和聯絡
1. JDK 用於開發,JRE 用於執行java程式 ;如果只是執行Java程式,可以只安裝JRE,無需安裝JDK。
2. JDk包含JRE,JDK 和 JRE 中都包含 JVM。
3. JVM 是 java 程式語言的核心並且具有平臺獨立性。
##二、位元組碼檔案詳解
官方文件地址:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.1
###2.1 位元組碼檔案的結構
```java
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`為例分析:
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/16%E8%BF%9B%E5%88%B6%E5%88%86%E8%A7%A3.png)
因此 u4 對應4個位元組,就是 `cafe babe`
接下來先分析 `ClassFile`的結構:
1. **magic**
在 class 檔案開頭的四個位元組, 存放著 class 檔案的魔數, 這個魔數是 class 檔案的標誌,是一個固定的值: 0xcafebabe 。 也就是說他是判斷一個檔案是不是 class 格式的檔案的標準, 如果開頭四個位元組不是 0xcafebabe , 那麼就說明它不是 class 檔案, 不能被 JVM 識別。
2. **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 的類檔案格式。範圍列表如下:
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/%E5%AD%97%E8%8A%82%E7%A0%81%E6%96%87%E4%BB%B6%E7%89%88%E6%9C%AC%E5%88%97%E8%A1%A8.png)
3. **constant_pool_count**
constant_pool_count 項的值等於 constant_pool 表中的條目數加1。如果 constant_pool 索引大於零且小於 constant_pool_count,則該索引被視為有效,但 CONSTANT_Long_info 和CONSTANT_Double_info 型別的常量除外。
4. **constant_pool**
constant_pool 是一個結構表,表示各種字串常量,類和介面名稱,欄位名稱以及在ClassFile 結構及其子結構中引用的其他常量。 每個 constant_pool 表條目的格式由其第一個“標籤”位元組指示。constant_pool 表的索引從1到 constant_pool_count-1。
Java虛擬機器指令不依賴於類,介面,類例項或陣列的執行時佈局。 相反,指令引用了constant_pool 表中的符號資訊。
所有 constant_pool 表條目均具有以下常規格式:
```java
cp_info {
u1 tag;
u1 info[];
}
```
constant_pool 表中的每個條目都必須以一個1位元組的標籤開頭,該標籤指示該條目表示的常量的種類。 常量有17種,在下表中列出,並帶有相應的標記。每個標籤位元組後必須跟兩個或多個位元組,以提供有關特定常數的資訊。 附加資訊的格式取決於標籤位元組,即info陣列的內容隨標籤的值而變化。
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/%E5%B8%B8%E9%87%8F%E7%B1%BB%E5%9E%8B.png)
5. **access_flags**
access_flags 項的值是標誌的掩碼,用於表示對該類或介面的訪問許可權和屬性。設定後,每個標誌的解釋在下表中指定。
![Alt](https://myblog-1258060977.cos.ap-beijing.myqcloud.com/cnblog/JVM/%E8%AE%BF%E9%97%AE%E7%B1%BB%E5%9E%8B.png)
6. **this_class**
this_class 專案的值必須是指向 constant_pool 表的有效索引。該索引處的 constant_pool 條目必須是代表此類檔案定義的類或介面的 CONSTANT_Class_info 結構。
```java
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
```
7. **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 結構。
8. **interfaces_count**
interfaces_count 專案的值給出了此類或介面型別的直接超介面的數量。
9. **interfaces[]**
介面表的每個值都必須是 constant_pool 表中的有效索引。interfaces [i]的每個值(其中0≤i
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`可得到:
```java
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"
```
對比下可以發現與我們人工解析的結果是一致的。
##小結
本文第一部分圍繞JVM的幾個常見的問題做了一些簡單介紹。第二部分詳細介紹了ClassFile的結構及 JVM 對 ClassFile 指定的規範(更多詳細的規範有興趣的讀者可檢視官方文件),接著按照規範進行了部分位元組碼的手動解析,並與 JVM 的解析結果進行了對比。個人認為作為偏應用層的programer沒必要去記憶這些“規範”,而是要跳出這些繁雜的規範掌握到以下幾點:
1. 會藉助官方文件對位元組碼檔案做簡單閱讀。
2. 理解位元組碼檔案在整個執行過程的角色和作用,其實就是一個“編解碼”的過程。javac將.java檔案按照JVM的規則生成位元組碼檔案,JVM按照規範解析位元組碼檔案為機器可執行的指令。
**參考文獻**:
https://blog.csdn.net/peng_zhanxuan/article/details/104329859
https://docs.oracle.com/javase/specs/jvms/se11/html/index.html
https://blog.csdn.net/weelyy/article/details/7