1. 程式人生 > >玩命學JVM(一)—認識JVM和位元組碼檔案

玩命學JVM(一)—認識JVM和位元組碼檔案

**本篇文章的思維導圖** ![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