玩命學JVM(一)—認識JVM和字節碼文件

本篇文章的思惟導圖
Althtml

1、JVM的簡單介紹

1.1 JVM是什麼?

JVM (java virtual machine),java虛擬機,是一個虛構出來的計算機,可是有本身完善的硬件結構:處理器、堆棧、寄存器等。java虛擬機是用於執行字節碼文件的。java

1.2 JAVA爲何能跨平臺?

首先咱們能夠問一個這樣的問題,爲何 C 語言不能跨平臺?以下圖:
Alt編程

C語言在不一樣平臺上的對應的編譯器會將其編譯爲不一樣的機器碼文件,不一樣的機器碼文件只能在本平臺中運行。數組

而java文件的執行過程如圖:
Alt
java經過javac將源文件編譯爲.class文件(字節碼文件),該字節碼文件遵循了JVM的規範,使其能夠在不一樣系統的JVM下運行。oracle

小結jvm

  • java 代碼不是直接在計算機上執行的,而是在JVM中執行的,不一樣操做系統下的 JVM 不一樣,可是會提供相同的接口。
  • javac 會先將 .java 文件編譯成二進制字節碼文件,字節碼文件與操做系統平臺無關,只面向 JVM, 注意同一段代碼的字節碼文件是相同的。
  • 接着JVM執行字節碼文件,不一樣操做系統下的JVM會將一樣的字節碼文件映射爲不一樣系統的API調用。
  • JVM不是跨平臺的,java是跨平臺的。

1.3 JVM爲何跨語言

前面提到".class文件是一種遵循了JVM規範的字節碼文件",那麼不難想到,只要另外一種語言也一樣了遵循了JVM規範,可將其源文件編譯爲.class文件,就也能在 JVM 上運行。以下圖:
Alt編程語言

1.4 JDK、JRE、JVM的關係

咱們看一下官方給的圖:
Alt工具

三者定義

  • 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 編程語言的核心而且具備平臺獨立性。

2、字節碼文件詳解

官方文檔地址:https://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"中的「u四、u2」等指的是每項數據的所佔的長度,u4表示佔4個字節,u2表示佔2個字節,以此類推。
  • .class文件是以16進制組織的,一個16進制位能夠用4個2進制位表示,一個2進制位是一個bit,因此一個16進制位是4個bit,兩個16進制位就是8bit = 1 byte。以Main.class文件的開頭cafe爲例分析:
    Alt
    所以 u4 對應4個字節,就是 cafe babe

接下來先分析 ClassFile的結構:this

  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
  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 表條目均具備如下常規格式:
    cp_info {
        u1 tag;
        u1 info[];
    }

constant_pool 表中的每一個條目都必須以一個1字節的標籤開頭,該標籤指示該條目表示的常量的種類。 常量有17種,在下表中列出,並帶有相應的標記。每一個標籤字節後必須跟兩個或多個字節,以提供有關特定常數的信息。 附加信息的格式取決於標籤字節,即info數組的內容隨標籤的值而變化。
Alt

  1. access_flags
    access_flags 項的值是標誌的掩碼,用於表示對該類或接口的訪問權限和屬性。設置後,每一個標誌的解釋在下表中指定。
    Alt

  2. this_class
    this_class 項目的值必須是指向 constant_pool 表的有效索引。該索引處的 constant_pool 條目必須是表明此類文件定義的類或接口的 CONSTANT_Class_info 結構。

    CONSTANT_Class_info {
          u1 tag;
          u2 name_index;
    }
  3. 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 結構。

  4. interfaces_count
    interfaces_count 項目的值給出了此類或接口類型的直接超接口的數量。

  5. interfaces[]
    接口表的每一個值都必須是 constant_pool 表中的有效索引。interfaces [i]的每一個值(其中0≤i <interfaces_count)上的 constant_pool 條目必須是 CONSTANT_Class_info 結構,該結構描述當前類或接口類型的直接超接口。

  6. fields_count
    字段計數器的值給出了 fields 表中 field_info 結構的數量。 field_info 結構表明此類或接口類型聲明的全部字段,包括類變量和實例變量。

  7. fields[]
    字段表中的每一個值都必須是field_info結構,以提供對該類或接口中字段的完整描述。 字段表僅包含此類或接口聲明的字段,不包含從超類或超接口繼承的字段。
    字段結構以下:

    field_info {
              u2             access_flags;
              u2             name_index;
              u2             descriptor_index;
              u2             attributes_count;
              attribute_info attributes[attributes_count];
          }
  8. methods_count
    方法計數器的值表示方法表中 method_info 結構的數量。

  9. 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];
        }
  10. attributes_count
    屬性計數器的值表示當前類的屬性表中的屬性數量。

  11. attributes[]
    注意,這裏的屬性並非Java代碼裏面的類屬性(類字段),而是Java源文件便已有特有的一些屬性(不要與 fields 混淆),屬性的結構:
    xml attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
    屬性列表:
    Alt

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不必去記憶這些「規範」,而是要跳出這些繁雜的規範掌握到如下幾點:

  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/78969412

相關文章
相關標籤/搜索