本篇文章的思惟導圖
html
JVM (java virtual machine),java虛擬機,是一個虛構出來的計算機,可是有本身完善的硬件結構:處理器、堆棧、寄存器等。java虛擬機是用於執行字節碼文件的。java
首先咱們能夠問一個這樣的問題,爲何 C 語言不能跨平臺?以下圖:
編程
C語言在不一樣平臺上的對應的編譯器會將其編譯爲不一樣的機器碼文件,不一樣的機器碼文件只能在本平臺中運行。數組
而java文件的執行過程如圖:
java經過javac將源文件編譯爲.class文件(字節碼文件),該字節碼文件遵循了JVM的規範,使其能夠在不一樣系統的JVM下運行。oracle
小結jvm
前面提到".class文件是一種遵循了JVM規範的字節碼文件",那麼不難想到,只要另外一種語言也一樣了遵循了JVM規範,可將其源文件編譯爲.class文件,就也能在 JVM 上運行。以下圖:
編程語言
咱們看一下官方給的圖:
工具
官方文檔地址:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.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]; }
.class
文件是以16進制組織的,一個16進制位能夠用4個2進制位表示,一個2進制位是一個bit,因此一個16進制位是4個bit,兩個16進制位就是8bit = 1 byte。以Main.class
文件的開頭cafe
爲例分析:cafe babe
接下來先分析 ClassFile
的結構:this
class file
文件的版本,若是 major_version 記做 M,minor_version 記做 m ,則該文件的版本號爲:M.m。所以,能夠按字典順序對類文件格式的版本進行排序,例如1.5 <2.0 <2.1。當且僅當v處於 Mi.0≤v≤Mj.m 的某個連續範圍內時,Java 虛擬機實現才能支持版本 v 的類文件格式。範圍列表以下: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]; }
屬性列表:
首先寫一段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不必去記憶這些「規範」,而是要跳出這些繁雜的規範掌握到如下幾點:
參考文獻:
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/78969412