JVM概述與字節碼


一、JAVA程序運行機制

1. 高級語言的運行機制

編譯型:c 、C++、FORTRAN、Pascal等解釋型:Ruby、Python編譯+解釋型:Visual
Basic,半編譯型語言,首先被編譯為P-代碼,並將解釋引擎封裝在可執行性程序內,當運行程序時,P-代碼會被解析成真正的二進制代碼。Visual
Basic編譯的EXE文件中,既有程序的啟動代碼,也有鏈接解釋程序的代碼,而這部分代碼負責啟動Visual
Basic解釋程序,再對VisualBasic代碼進行解釋並執行。虛擬機:Java/Groovy/Scala,.Net,

2. Java程序的運行機制和JVM

Java語言比較特殊,程序運行的窗口是JVM虛擬機(Java Virutal Machine)。


JVM是可運行Java字節文件的虛擬計算機。所有平臺上的JVM向編譯器提供相同的編程接口,而編譯器只需要面向虛擬機,生成虛擬機能理解的字節碼(可以理解成JVM使用的機器碼),然後由虛擬機來解釋執行。

在一些虛擬機的實現中,還會使用just-in-time的編譯器將虛擬機代碼進行進一步的編譯,轉換成特定系統的機器碼執行,從而提高執行效率。

已故的Sun公司制定的Java虛擬機規範在技術上規定了JVM的統一標準,具體定義了JVM的如下細節:

指令集寄存器類文件的格式棧垃圾回收堆存儲區

可以看出虛擬機實現了一個比較完整的操作系統內核,

3. JVM指令集

JVM的指令集目前的官網文檔在:
https://docs.oracle.com/javase/specs/jvms/se12/html/jvms-6.html
JAVA 指令集目前有2百條左右指令(8bit,總數不會超過255個),如果按物理的CPU指令劃分,也算是複雜指令集了。












4. 寄存器

JVM的指令集是基於棧,而不是基於寄存器的,其用到的寄存器很少。

JAVA虛擬機中的寄存器是PC(程序運行計數器)其它寄存器利用物理CPU的寄存器:堆棧指針寄存器、框架寄存器、變量寄存器。

JVM讓物理CPU直接執行Java程序所對應的目標機器碼,而不需要實現一個虛擬的CPU。讓CPU執行java的代碼時,需要對CPU場景進行上下文切換,這部分需要佔用比較大的CPU資源。

JVM調用函數的時候,Java函數的代碼並沒有被存放到代碼段中,而是被放在了一個code緩存中,每一個Java函數的代碼塊在這個code緩存中都會有一個索引位置,最終JVM會跳轉到這個索引位置處執行Java函數調用。同時Java的函數一定是封裝在類中的,因此JVM在執行函數調用時,還需要通過類尋址等一系列運算最終才能定位這個入口。

二、字節碼文件格式

1. java編譯器流程:

2. 查看字節碼示例

寫一段簡單的Java程序:

<code>package com.xundh; public class Main { public static void main(String[] args) { int c=add(1,2); System.out.println("Hello World"); System.out.println("c="+c); } public static int add(int a,int b){ return a+b; } } /<code>

執行下面命令:

<code>javac Main.java javap -verbose Main.class/<code>

分析字節碼文件後輸出如下內容:

<code>D:\Documents\Downloads\learn-java\src\com\xundh>javap -verbose Main.class Classfile /D:/Documents/Downloads/learn-java/src/com/xundh/Main.class Last modified 2020-9-15; size 947 bytes MD5 checksum 063ed126168378bb07d6610d353ad144 Compiled from "Main.java" public class com.xundh.Main minor version: 0 // 子版本 major version: 55 // 主版本 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #8.#19 // java/lang/Object."":()V #2 = Methodref #7.#20 // com/xundh/Main.add:(II)I add方法 #3 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #4 = String #23 // Hello World #5 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #6 = InvokeDynamic #0:#29 // #0:makeConcatWithConstants:(I)Ljava/lang/String; #7 = Class #30 // com/xundh/Main #8 = Class #31 // java/lang/Object #9 = Utf8 #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 main // 方法 #14 = Utf8 ([Ljava/lang/String;)V #15 = Utf8 add // 方法 #16 = Utf8 (II)I #17 = Utf8 SourceFile // 源文件名 #18 = Utf8 Main.java #19 = NameAndType #9:#10 // "":()V #20 = NameAndType #15:#16 // add:(II)I #21 = Class #32 // java/lang/System #22 = NameAndType #33:#34 // out:Ljava/io/PrintStream; #23 = Utf8 Hello World #24 = Class #35 // java/io/PrintStream #25 = NameAndType #36:#37 // println:(Ljava/lang/String;)V #26 = Utf8 BootstrapMethods #27 = MethodHandle #6:#38 // invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lan g/Object;)Ljava/lang/invoke/CallSite; #28 = String #39 // c=╔ #29 = NameAndType #40:#41 // makeConcatWithConstants:(I)Ljava/lang/String; #30 = Utf8 com/xundh/Main #31 = Utf8 java/lang/Object #32 = Utf8 java/lang/System #33 = Utf8 out #34 = Utf8 Ljava/io/PrintStream; #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Ljava/lang/String;)V #38 = Methodref #42.#43 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Lja va/lang/invoke/CallSite; #39 = Utf8 c=╔ #40 = Utf8 makeConcatWithConstants #41 = Utf8 (I)Ljava/lang/String; #42 = Class #44 // java/lang/invoke/StringConcatFactory #43 = NameAndType #40:#48 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; #44 = Utf8 java/lang/invoke/StringConcatFactory #45 = Class #50 // java/lang/invoke/MethodHandles$Lookup #46 = Utf8 Lookup #47 = Utf8 InnerClasses #48 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; #49 = Class #51 // java/lang/invoke/MethodHandles #50 = Utf8 java/lang/invoke/MethodHandles$Lookup #51 = Utf8 java/lang/invoke/MethodHandles { public com.xundh.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 3: 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: iconst_1 // 前面是偏移地址,後面是操作指令 1: iconst_2 2: invokestatic #2 // Method add:(II)I 調用靜態函數 5: istore_1 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #4 // String Hello World 11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 17: iload_1 18: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String; 23: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 26: return LineNumberTable: // 源碼與字節碼對應關係 line 6: 0 line 7: 6 line 8: 14 line 9: 26 public static int add(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: iadd 3: ireturn LineNumberTable: line 11: 0 } SourceFile: "Main.java" InnerClasses: public static final #46= #45 of #49; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles BootstrapMethods: 0: #27 invokestatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite ; Method arguments: #28 c=╔ /<code>

對應的十六進制文件解析:


部分字段解釋:

u4 : MagicNumber 前4個字節 固定值 0xCAFEBABE2個u2 : Version 版本號4個字節,跟在MagicNumber後面1個u2 : Constant_pool 常量數量接下來是常量池Fields+attributes 字段信息Methods + attributes 方法信息


3. 常量池

常量池的數據通常有兩種類型:

字面量,如字符串、final修飾的常量符號引用: 類/接口的全限定名、方法的名稱和描述、字段的名稱和描述等

三、反彙編字節碼

下面的操作需要 hsdis的支持,下載地址:
https://sourceforge.net/projects/fcml/files/fcml-1.1.3/
把dll放到 \jdk1.8.0_31\jre\bin\server 目錄下。
在IDEA中設置 VM options:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
編譯就會顯示反彙編的代碼。


反彙編的示例:


可以看到字節碼到彙編以後的代碼數量是暴增的。

四、關於動態修改字節碼

由於java的字節碼文件是有明確規格的,所以可以通過對字節碼的修改來動態的修改類,這就是AOP技術的核心。但直接對字節碼進行修改非常困難,需要對字節碼非常熟悉。一些第三方的庫可以簡化這種修改,如:

ASM 基於 Java 字節碼層面的代碼分析和修改工具,可以直接產生二進制class文件,也可以在類被加載到JVM前動態改變類行為;javassist 開源的分析、編輯和創建Java字節碼的類庫,是jboss的一個子項目cglib 一個高性能的代碼生成包,實現JDK的動態代理,被應用在Spring AOP、dynaop、Hibernate等框架中。BCEL Apache Byte Code Engineering Library,實現對字節碼文件的功能擴充。