天天看點

JVM(1)——位元組碼

1、JVM基礎

1.1、JDK,JRE,JVM關系

JDK

  • JDK(Java Development Kit) 是用于開發 Java 應用程式的軟體開發工具集合,包括 了 Java 運作時的環境(JRE)、解釋器(Java)、編譯器(javac)、Java 歸檔 (jar)、文檔生成器(Javadoc)等工具。簡單的說我們要開發Java程式,就需要安 裝某個版本的JDK工具包。

JRE

  • JRE(Java Runtime Enviroment )提供 Java 應用程式執行時所需的環境,由 Java 虛拟機(JVM)、核心類、支援檔案等組成。簡單的說,我們要是想在某個機器上運 行Java程式,可以安裝JDK,也可以隻安裝JRE,後者體積比較小。

JVM

  • Java Virtual Machine(Java 虛拟機)有三層含義,分别是: JVM規範要求 滿足 JVM 規範要求的一種具體實作(一種計算機程式) 一個 JVM 運作執行個體,在指令提示符下編寫 Java 指令以運作 Java 類時,都會建立一 個 JVM 執行個體,我們下面如果隻記到JVM則指的是這個含義;如果我們帶上了某種JVM 的名稱,比如說是Zing JVM,則表示上面第二種含義

JDK 與 JRE、JVM 之間的關系

JDK > JRE > JVM

  • JDK = JRE + 開發工具
  • JRE = JVM + 類庫
JVM(1)——位元組碼

三者在開發運作Java程式時的互動關系: 簡單的說,就是通過JDK開發的程式,編譯以後,可以打包分發給其他裝有JRE的機器上去運作 而運作的程式,則是通過java指令啟動的一個JVM執行個體,代碼邏輯的執行都運作在這個JVM執行個體上

JVM(1)——位元組碼

Java程式的開發運作過程為: 我們利用 JDK (調用 Java API)開發Java程式,編譯成位元組碼或者打包程式。然後可以用 JRE 則啟動一個JVM執行個體,加載、驗證、執行 Java 位元組碼以及依賴庫,運作Java程式 而JVM 将程式和依賴庫的Java位元組碼解析并變成本地代碼執行,産生結果

2、Java位元組碼

2.1、定義

  • Java位元組碼是JVM的指令集
  • JVM加載位元組碼格式的class檔案,檢驗之後通過JIT編譯器轉換為本地機器代碼執行。
  • Java位元組碼就是JVM執行的指令格式

2.2、操作碼

  • Java bytecode 由單位元組( byte )的指令組成,理論上最多支援256 個操作碼(opcode)。實際上Java隻使用了200左右的操作碼, 還有一些操作碼則保留給調試操作。
  • 操作碼, 下面稱為 指令 , 主要由 類型字首 和 操作名稱 兩部分組成。

2.3、指令性質

    1. 棧操作指令,包括與局部變量互動的指令
    1. 程式流程控制指令
    1. 對象操作指令,包括方法調用指令
    1. 算術運算以及類型轉換指令

2.4、位元組碼的分類

  1. 棧操作指令,包括與局部變量互動的指令
  2. 程式流程控制指令
  3. 對象操作指令,包括方法調用指令
  4. 算術運算以及類型轉換指令

2.5、位元組碼閱讀

源碼:

package com.zhz.bytecode;

/**
 * @author zhouhengzhe
 * @Description: TODO
 * @date 2021/9/15下午7:57
 * @since
 */

public class HelloByteCode {
    public static void main(String[] args) {
        HelloByteCode helloByteCode=new HelloByteCode();
        System.out.println(helloByteCode);
    }
}
           

第一步先:

javac HelloByteCode.java    =》  會生成一個HelloByteCode.class
           
  • Javac 不指定 -d 參數編譯後生成的 .class 檔案預設和源代碼在同一個目錄。 注意: javac 工具預設開啟了優化功能, 生成的位元組碼中沒有局部變量表(LocalVariableTable),相當于 局部變量名稱被擦除。如果需要這些調試資訊, 在編譯時請加上 -g 選項。
  • JDK自帶工具的詳細用法, 請使用: javac -help 或者 javap -help 來檢視;

打開 HelloByteCode.class

cafe babe 0000 0034 001c 0a00 0600 0f07
0010 0a00 0200 0f09 0011 0012 0a00 1300
1407 0015 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 0012 4865 6c6c
6f42 7974 6543 6f64 652e 6a61 7661 0c00
0700 0801 001e 636f 6d2f 7a68 7a2f 6279
7465 636f 6465 2f48 656c 6c6f 4279 7465
436f 6465 0700 160c 0017 0018 0700 190c
001a 001b 0100 106a 6176 612f 6c61 6e67
2f4f 626a 6563 7401 0010 6a61 7661 2f6c
616e 672f 5379 7374 656d 0100 036f 7574
0100 154c 6a61 7661 2f69 6f2f 5072 696e
7453 7472 6561 6d3b 0100 136a 6176 612f
696f 2f50 7269 6e74 5374 7265 616d 0100
0770 7269 6e74 6c6e 0100 1528 4c6a 6176
612f 6c61 6e67 2f4f 626a 6563 743b 2956
0021 0002 0006 0000 0000 0002 0001 0007
0008 0001 0009 0000 001d 0001 0001 0000
0005 2ab7 0001 b100 0000 0100 0a00 0000
0600 0100 0000 0a00 0900 0b00 0c00 0100
0900 0000 3000 0200 0200 0000 10bb 0002
59b7 0003 4cb2 0004 2bb6 0005 b100 0000
0100 0a00 0000 0e00 0300 0000 0c00 0800
0d00 0f00 0e00 0100 0d00 0000 0200 0e
           
javap -c -verbose HelloByteCode
Classfile /Users/mac/Documents/ideaproject/Java/Java基礎/HelloByteCode.class
  Last modified 2021-9-15; size 447 bytes
  MD5 checksum 6631029ab59bc19003c854c3360c6a70
  Compiled from "HelloByteCode.java"
public class com.zhz.bytecode.HelloByteCode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Class              #16            // com/zhz/bytecode/HelloByteCode
   #3 = Methodref          #2.#15         // com/zhz/bytecode/HelloByteCode."<init>":()V
   #4 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/Object;)V
   #6 = Class              #21            // 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               HelloByteCode.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Utf8               com/zhz/bytecode/HelloByteCode
  #17 = Class              #22            // java/lang/System
  #18 = NameAndType        #23:#24        // out:Ljava/io/PrintStream;
  #19 = Class              #25            // java/io/PrintStream
  #20 = NameAndType        #26:#27        // println:(Ljava/lang/Object;)V
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/System
  #23 = Utf8               out
  #24 = Utf8               Ljava/io/PrintStream;
  #25 = Utf8               java/io/PrintStream
  #26 = Utf8               println
  #27 = Utf8               (Ljava/lang/Object;)V
{
  public com.zhz.bytecode.HelloByteCode();
    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 10: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/zhz/bytecode/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        15: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 15
}
SourceFile: "HelloByteCode.java"
           
上面的位元組碼詳解

super():

JVM(1)——位元組碼

無參構造函數的參數個數居然不是0: stack=1, locals=1,args_size=1 。 這是因為在 Java 中, 如果是靜态方法則沒有 this 引用。 對于非靜态方法, this 将被配置設定到局部變量表的第0号槽位中

我們通過檢視編譯後的class檔案證明了其中存在預設構造函數,是以這是Java編譯器生成的, 而不是運行時JVM自動生成的。** 自動生成的構造函數,其方法體應該是空的**,但這里看到里面有一些指令。為什麼呢? 再次回顧Java知識, 每個構造函數中都會先調用 super 類的構造函數對吧? 但這不是JVM自動執行的, 而是由程式指令控制,是以預設構造函數中也就有一些位元組碼指令來幹這個事情。

java/lang/Object:

JVM(1)——位元組碼
預設繼承了 Object 類

main

JVM(1)——位元組碼

可以看到方法描述: **([Ljava/lang/String;)V **:

  • 其中小括号内是入參資訊/形參資訊,
  • 左方括号表述數組,
  • L 表示對象,
  • 後面的 java/lang/String 就是類名稱
  • 小括号後面的 V 則表示這個方法的傳回值是 void
  • 方法的通路标志也很容易理解 **flags: ACC_PUBLIC, ACC_STATIC ,表示public和static **
  • 還可以看到執行該方法時需要的棧(stack)深度是多少,需要在局部變量表中保留多少個槽位, 還有方法的參 數個數: stack=2, locals=2, args_size=1 。把上面這些整合起來其實就是一個方法:
  • **public static void main(java.lang.String[]); **
  • 注:實際上我們一般把一個方法的修飾符+名稱+參數類型清單+傳回值類型,合在一起叫“方法簽名”, 即這些資訊可以完整的表示一個方法。
  • main 方法中建立了該類的一個實例, 然後就return了

常量池:

常量池 大家應該都聽說過, 英文是 Constant pool 。這里做一個強調: 大多數時候指的是運行時常量池 。但運行時常量池里面的常量是從哪里來的呢? 主要就是由 class 檔案中的常量池結構體組成的。
JVM(1)——位元組碼
  • 其中顯示了很多關于class檔案資訊: 編譯時間, MD5校驗和, 從哪個 .java 源檔案編譯得來,符合哪個版本的Java語言規範等等。
  • 還可以看到 ACC_PUBLIC 和 ACC_SUPER 通路标志符。
  • ACC_PUBLIC 标志很容易理解:這個類是 public 類,是以用這個标志來表示。 但 ACC_SUPER 标志是怎麼回事呢? 這就是曆史原因, JDK1.0 的BUG修正中引入 ACC_SUPER 标志來修正 invokespecial 指令調用 super 類方法的問題,從 Java 1.1 開始, 編譯器一般都會自動生成ACC_SUPER 标志。

解釋 #1, #2, #3

JVM(1)——位元組碼
JVM(1)——位元組碼
#3=#2.#15=#16.#15=#16.#7:#8=com/zhz/bytecode/HelloByteCode.""????)V

總結一下,常量池就是一個常量的大字典,使用編号的方式把程式里用到的各類常量統一管理起來,這樣在位元組碼操作里,隻需要引用編号即可。

#1 = Methodref #6.#15 // java/lang/Object.""????)V

解讀如下:

  • #1 常量編号, 該檔案中其他地方可以引用。
  • = 等号就是分隔符.
  • Methodref 表明這個常量指向的是一個方法;具體是哪個類的哪個方法呢?
    • 類指向的 #4 , 方法簽 名指向的 #13 ; 當然雙斜線注釋後面已經解析出來可讀性比較好的說明了。

2.5、線程棧與位元組碼執行模型

JVM是一台基于棧的計算機器。每個線程都有一個獨屬于自己的線程棧(JVM stack),用于存儲棧幀 (Frame)。每一次方法調用,JVM都會自動建立一個棧幀。 棧幀 由 操作數棧 ,** 局部變量數組** 以及一 個** class引用** 組成。 class引用 指向目前方法在運行時常量池中對應的class)。

JVM(1)——位元組碼

局部變量數組 也稱為 局部變量表 (LocalVariableTable), 其中包含了方法的參數,以及局部變量。 局部變 量數組的大小在編譯時就已經确定: 和局部變量+形參的個數有關,還要看每個變量/參數占用多少個位元組。** 操作數棧是一個LIFO結構的棧**, 用于壓入和彈出值。 它的大小也在編譯時确定。

有一些操作碼/指令可以将值壓入“操作數棧”; 還有一些操作碼/指令則是從棧中擷取操作數,并進行處理, 再将結果壓入棧。操作數棧還用于接收調用其他方法時傳回的結果值。

2.6、方法體中的位元組碼解讀

main方法裡面的位元組碼:

0: new           #2                  // class com/zhz/bytecode/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        15: return
           
  • 有一部分操作碼會附帶有操作數, 也會占用位元組碼數組中的空間
  • new 就會占用三個槽位: 一個用于存放操作碼指令自身,兩個用于存放操作數
  • 下一條指令 dup 的索引從 3 開始
JVM(1)——位元組碼

從位元組碼中可以看到

JVM(1)——位元組碼

2.6、位元組碼的運作時結構

  • JVM 是一台基于棧的計算機器。
  • 每個線程都有一個獨屬于自己的線程棧(JVM Stack),用于存儲

    棧幀(Frame)。

  • 每一次方法調用、JVM 都會自動建立一個棧幀。
  • 棧幀由操作數棧、 局部變量數組以及一個 Class 引用組成。

    組成。

  • Class 引用指向目前方法在運作時常量池中對應的 Class。