JVM學習筆記——類載入和位元組碼技術篇

JVM學習筆記——類載入和位元組碼技術篇

在本系列內容中我們會對JVM做一個系統的學習,本片將會介紹JVM的類載入和位元組碼技術部分

我們會分為以下幾部分進行介紹:

  • 類文件結構
  • 位元組碼指令
  • 編譯期處理
  • 類載入階段
  • 類載入器
  • 運行期優化

類文件結構

這一小節我們將簡單介紹一下類的文件結構部分,簡單閱讀一下以下內容即可

整體文件展示

首先我們通過一個簡單的HelloWorld文件來進行類文件結構介紹

首先我們給出Java文件程式碼:

package cn.itcast.jvm.t5;
// HelloWorld 示例
public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("hello world");
}

我們如果想要獲取底層二進位程式碼,需要在out文件下輸入以下命令:

// 獲得底層二進位程式碼
javac -parameters -d . HellowWorld.java

然後我們就可以獲得二進位程式碼:

// 當然目前你是完全看不懂的,我們這裡只需要大概了解結構即可,不需要解讀
[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

類文件結構展示

首先我們給出類文件結構的整體展示:

ClassFile {
    // 魔數
    u4 magic;
    
    // 類文件版本
    u2 minor_version;
    u2 major_version;
    
    // 類文件常量池
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    
    // 類文件的類型(public或private)
    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];
}

魔數資訊

首先我們給出魔數定義:

  • 0~3 位元組,表示它是否是【class】類型的文件

我們給出實例展示:

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

版本資訊

首先我們給出版本定義:

  • 4~7 位元組,表示類的版本 00 34(52) 表示是 Java 8

我們給出實例展示:

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

常量池資訊

首先我們給出常量池定義:

  • 8~9 位元組,表示常量池長度,00 23 (35) 表示常量池有 #1~#34項,注意 #0 項不計入,也沒有值

我們給出實例展示:

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

我們給出常量池的類型對應的十六進位標號:

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

然後下述的34項全部都是常量內容,我們的常量通常分為以下幾種:

資訊表示 資訊表示名 位數 含義
0a Method 資訊 3 資訊表示:調用類名:調用方法名
09 Field 資訊 3 資訊表示:調用類名:調用靜態方法名
08 字元串常量名稱 2 資訊表示:調用常量池位置
07 Class 資訊 2 資訊表示:調用常量池位置
01 utf8 串 3 資訊表示:字元長度:字元意義

我們給出一些實例:

// 第#1項 0a 表示一個 Method 資訊,00 06 和 00 15(21) 
// 表示它引用了常量池中 #6 和 #21 項來獲得這個方法的【所屬類】和【方法名】
0a 00 06 00 15

// 第#2項 09 表示一個 Field 資訊,00 16(22)和 00 17(23) 
//表示它引用了常量池中 #22 和 # 23 項來獲得這個成員變數的【所屬類】和【成員變數名】
09 00 16 00 17
    
// 第#3項 08 表示一個字元串常量名稱,00 18(24)表示它引用了常量池中 #24 項
08 00 18 
    
// 第#5項 07 表示一個 Class 資訊,00 1b(27) 表示它引用了常量池中 #27 項
07 00 1b
    
// 第#7項 01 表示一個 utf8 串,00 06 表示長度,3c 69 6e 69 74 3e 是【 <init> 】
00 06 3c 69 6e 69 74 3e

訪問標識與繼承資訊

首先我們給出訪問標識與繼承資訊定義:

  • 一個位元組表示該 class 是一個類的資訊:00 21 公共的
  • 一個位元組表示根據常量池中位置找到本類全限定名:00 05 表示常量池#5
  • 一個位元組表示根據常量池中位置找到父類全限定名 :00 06 表示常量池#6
  • 一個位元組表示介面的數量: 00 00 表示介面數為0

我們給出訪問標識與繼承資訊的一些資訊列表:

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public ; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final ; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract ; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

成員變數資訊

首先我們給出成員變數資訊定義:

  • 表示成員變數數量,本類為 0

我們給出實例展示:

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

我們給出成員變數的一些資訊列表:

FieldType Type Interpretation
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L ClassName ; reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

方法資訊

首先我們給出方法資訊定義:

  • 表示方法數量,本類為 2

我們給出實例展示:

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

一個方法由 訪問修飾符,名稱,參數描述,方法屬性數量,方法屬性組成,由於過於複雜這裡不做展示

附加屬性

首先我們給出方法資訊定義:

  • 00 01 表示附加屬性數量
  • 00 13 表示引用了常量池 #19 項,即【SourceFile】
  • 00 00 00 02 表示此屬性的長度
  • 00 14 表示引用了常量池 #20 項,即【HelloWorld.java】

我們給出實例展示:

  • 00 01 00 13 00 00 00 02 00 14

位元組碼指令

這一節我們將詳細介紹位元組碼指令以及分析Java底層程式碼

位元組碼指令介紹

我們首先對之前的HelloWorld中的兩個指令進行介紹

第一個指令是:

  • 構造方法的位元組碼指令 :public cn.itcast.jvm.t5.HelloWorld();

其二進位程式碼為:

  • 2a b7 00 01 b1

我們對其進行解釋:

  • 2a => aload_0 載入 slot 0 的局部變數,即 this,做為下面的 invokespecial 構造方法調用的參數
  • b7 => invokespecial 預備調用構造方法,哪個方法呢?
  • 00 01 引用常量池中 #1 項,即【 Method java/lang/Object.”<init>”😦)V 】
  • b1 表示返回

第二個指令是:

  • 主方法的位元組碼指令:public static void main(java.lang.String[]);

其二進位程式碼為:

  • b2 00 02 12 03 b6 00 04 b1

我們對其進行解釋:

  • b2 => getstatic 用來載入靜態變數,哪個靜態變數呢?

  • 00 02 引用常量池中 #2 項,即【Field java/lang/System.out:Ljava/io/PrintStream;】

  • 12 => ldc 載入參數,哪個參數呢?

  • 03 引用常量池中 #3 項,即 【String hello world】

  • b6 => invokevirtual 預備調用成員方法,哪個方法呢?

  • 00 04 引用常量池中 #4 項,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】

  • b1 表示返回

Javap工具介紹

我們如果採用二進位程式碼來查看其底層數據就會顯得繁雜且麻煩

所以Java為我們提供了具體的工具,隸屬於JVM的工具,可以直接在out文件下使用:

// javap 反編譯工具
[root@localhost ~]# javap -v HelloWorld.class

然後我們就可以得到HelloWorld的反編譯文件:

// 雖然依舊是底層程式碼,但這種閱讀方式就比較舒服

// 魔數
Classfile /root/HelloWorld.class
    Last modified Jul 7, 2019; size 597 bytes
    MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc
    Compiled from "HelloWorld.java"
// 版本
public class cn.itcast.jvm.t5.HelloWorld
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
    #1 = Methodref #6.#21 // java/lang/Object."<init>":()V
    #2 = Fieldref #22.#23 //
java/lang/System.out:Ljava/io/PrintStream;
    #3 = String #24 // hello world
    #4 = Methodref #25.#26 // java/io/PrintStream.println:
(Ljava/lang/String;)V
    #5 = Class #27 // cn/itcast/jvm/t5/HelloWorld
    #6 = Class #28 // java/lang/Object
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 LocalVariableTable
    #12 = Utf8 this
    #13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;#14 = Utf8 main
    #15 = Utf8 ([Ljava/lang/String;)V
    #16 = Utf8 args
    #17 = Utf8 [Ljava/lang/String;
    #18 = Utf8 MethodParameters
    #19 = Utf8 SourceFile
    #20 = Utf8 HelloWorld.java
    #21 = NameAndType #7:#8 // "<init>":()V
    #22 = Class #29 // java/lang/System
    #23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
    #24 = Utf8 hello world
    #25 = Class #32 // java/io/PrintStream
    #26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
    #27 = Utf8 cn/itcast/jvm/t5/HelloWorld
    #28 = Utf8 java/lang/Object
    #29 = Utf8 java/lang/System
    #30 = Utf8 out
    #31 = Utf8 Ljava/io/PrintStream;
    #32 = Utf8 java/io/PrintStream
    #33 = Utf8 println
    #34 = Utf8 (Ljava/lang/String;)V
                
// 方法執行
{
	public cn.itcast.jvm.t5.HelloWorld();
    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 4: 0
	LocalVariableTable:
        Start Length Slot Name Signature
        0        5     0  this Lcn/itcast/jvm/t5/HelloWorld;
	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 6: 0
			line 7: 8
        // 局部變數池
		LocalVariableTable:
			Start Length Slot Name Signature
			0		 9 	   0  args [Ljava/lang/String;
	MethodParameters:
	Name Flags
	args
}

圖解方法執行流程

我們首先給出一串簡單的Java程式碼:

package cn.itcast.jvm.t3.bytecode;

/**
 * 演示 位元組碼指令 和 操作數棧、常量池的關係
 */
public class Demo3_1 {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

我們再給出javap反編譯後的程式碼:

Classfile /E:/編程內容/JVM/資料-解密JVM/程式碼/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2022-11-2; size 635 bytes
  MD5 checksum 1a6413a652bcc5023f130b392deb76a1
  Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/itcast/jvm/t3/bytecode/Demo3_1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo3_1.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_1
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_1();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 6
        line 11: 10
        line 12: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo3_1.java"

下面我們通過圖解來執行底層結構變化:

  1. 常量池載入運行時常量池

  1. 方法位元組碼載入方法區

  1. main 執行緒開始運行,分配棧幀記憶體
這裡的綠色塊塊是局部變數表,裡面用於存放局部變數,大小為4,前面javap有標記
這裡的青色塊塊是操作數棧,我們的操作數的任何操作都要在裡面進行,大小為2,前面javap有標記

  1. 執行方法bipush 10
將一個 byte 壓入操作數棧(其長度會補齊 4 個位元組),類似的指令還有:

- sipush 將一個 short 壓入操作數棧(其長度會補齊 4 個位元組)
- ldc 將一個 int 壓入操作數棧
- ldc2_w 將一個 long 壓入操作數棧(分兩次壓入,因為 long 是 8 個位元組)

這裡小的數字都是和位元組碼指令存在一起,超過 short 範圍的數字存入了常量池

  1. 執行方法istore_1
將操作數棧頂數據彈出,存入局部變數表的 slot 1

  1. 執行方法ldc #3
從常量池載入 #3 數據到操作數棧
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 實際是在編譯期間計算好的

  1. 執行方法istore_2

  1. 執行方法iload_1 iload_2

  1. 執行方法iadd

  1. 執行方法istore_3

  1. 執行方法getstatic #4
這裡需要注意getstatic引用了常量池裡存放的System.out的引用對象地址
然後我們到堆里去尋找該對象,找到該對象後將該對象的引用放到操作數棧中進行操作

  1. 執行方法iload_3

  1. 執行方法invokevirtual #5
第一步操作:
找到常量池 #5 項
定位到方法區 java/io/PrintStream.println:(I)V 方法
生成新的棧幀(分配 locals、stack等)
傳遞參數,執行新棧幀中的位元組碼

第二步操作:
執行完畢,彈出棧幀
清除 main 操作數棧內容

  1. 執行方法return
完成 main 方法調用,彈出 main 棧幀
程式結束

方法i++底層實現

目的:

  • 從位元組碼角度分析 a++ 相關題目

源碼:

package cn.itcast.jvm.t3.bytecode;

/**
 * 從位元組碼角度分析 a++  相關題目
 */
public class Demo3_2 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}

位元組碼:

Classfile /E:/編程內容/JVM/資料-解密JVM/程式碼/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_2.class
  Last modified 2022-11-2; size 610 bytes
  MD5 checksum 5f6a35e5b9bb88d08249958a8d2ab043
  Compiled from "Demo3_2.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #4 = Class              #27            // cn/itcast/jvm/t3/bytecode/Demo3_2
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_2;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo3_2.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = Class              #29            // java/lang/System
  #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_2
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_2();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_2;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 18
        line 11: 25
        line 12: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
}
SourceFile: "Demo3_2.java"

相關知識點補充:

// 注意 iinc 指令是直接在局部變數 slot 上進行運算
// a++和++a的區別是先執行 iload 還是 先執行 iinc
// a++ 先執行iload
// ++a 先執行iinc

相關圖示展示:

條件判斷指令

首先我們給出條件判斷的相關指令集合:

指令 助記符 含義
0x99 ifeq 判斷是否 == 0
0x9a ifne 判斷是否 != 0
0x9b iflt 判斷是否 < 0
0x9c ifge 判斷是否 >= 0
0x9d ifgt 判斷是否 > 0
0x9e ifle 判斷是否 <= 0
0x9f if_icmpeq 兩個int是否 ==
0xa0 if_icmpne 兩個int是否 !=
0xa1 if_icmplt 兩個int是否 <
0xa2 if_icmpge 兩個int是否 >=
0xa3 if_icmpgt 兩個int是否 >
0xa4 if_icmple 兩個int是否 <=
0xa5 if_acmpeq 兩個引用是否 ==
0xa6 if_acmpne 兩個引用是否 !=
0xc6 ifnull 判斷是否 == null
0xc7 ifnonnull 判斷是否 != null

我們對上述內容做簡單說明:

  • byte,short,char 都會按 int 比較,因為操作數棧都是 4 位元組
  • goto 用來進行跳轉到指定行號的位元組碼

源碼:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_3 {
    public static void main(String[] args) {
        int a = 0;
        if(a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}

我們將重要的位元組碼內容單獨調下來講解:

// 產生一個0
0: iconst_0
// 放入到局部變數池
1: istore_1
// 取出0    
2: iload_1
// ifne:判斷是否 != 0
// 進行判斷是否不為0,如果不為0直接跳轉12,如果不是繼續執行
3: ifne 12
// 放入一個10,然後存入到原本a的位置
6: bipush 10
8: istore_1
// 直接跳轉到return
9: goto 15
// 這裡是如果不為0的邏輯:放入一個20然後存到a的位置
12: bipush 20
14: istore_1
// 程式碼結束
15: return

循環控制指令

我們循環控制指令實際上還是採用條件判斷語句的指令進行操作

while源碼:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_4 {
    public static void main(String[] args) {
        int a = 0;
        while (a < 10) {
            a++;
        }
    }
}

我們將while中重要的位元組碼內容單獨調下來講解:

0: iconst_0
1: istore_1
// 首先取出a的值,再放入一個10,然後採用if_icmpge判斷a是否>=10,如果是跳轉return結束,如果不是執行下述操作
2: iload_1
3: bipush 10
5: if_icmpge 14
// 這裡是自加操作,注意是在局部變數中執行,這時局部變數的a+1,但是操作數棧的a值未發生變化
8: iinc 1, 1
// 我們回到2操作,重新取a,取10,並再次進行比較
11: goto 2
14: return

dowhile源碼:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_5 {
    public static void main(String[] args) {
        int a = 0;
        do {
            a++;
        } while (a < 10);
    }
}

我們將dowhile中重要的位元組碼內容單獨調下來講解:

0: iconst_0
1: istore_1
// 我們首先對a的值進行一次自加操作,然後再取a和10
2: iinc 1, 1
5: iload_1
6: bipush 10
// 在這裡進行進行判斷,如果符合條件就回到第二步不斷重複
8: if_icmplt 2
11: return

for源碼:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_6 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {

        }
    }
}

我們將for中重要的位元組碼內容單獨調下來講解:

// 我們會發現for和while的源碼是完全一致的~
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

構造方法

我們先來介紹構造方法的構造原理:

  • 編譯器會按從上至下的順序,收集所有 static 靜態程式碼塊和靜態成員賦值的程式碼,合併為一個特殊的方法 <cinit>()V
  • <cinit>()V 方法會在類載入的初始化階段被調用 ,但原始構造方法<init>()V 內的程式碼總是在最後

首先我們來介紹類的構造方法:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_8_1 {

	static int i = 10;
    
    static {
        i = 20;
    }


    static {
        i = 30;
    }

    

    public static void main(String[] args) {
        System.out.println(Demo3_8_1.i);
    }
}

然後我們查看重要的位元組碼部分:

// 這裡首先將最上面的構造語句static int i = 10;讀取,並輸入10為i的值
0: bipush 10
2: putstatic #2 // Field i:I
// 這裡將中間的構造方法讀取,i=20
5: bipush 20
7: putstatic #2 // Field i:I
// 這裡將最後的構造方法讀取,i=30
10: bipush 30
12: putstatic #2 // Field i:I
// 所以最後我們的類中的i為30
15: return

然後同理我們來查看存在原始構造方法的類:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_8_2 {


    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    // 這個就是原始構造方法
    public Demo3_8_2(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Demo3_8_2 d = new Demo3_8_2("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
}

我們來查看重要的位元組碼部分:

// 首先他們調用了 super.<init>()V,分別按順序給ab進行賦值
0: aload_0
1: invokespecial #1 // super.<init>()V
4: aload_0
5: ldc #2 // <- "s1"
7: putfield #3 // -> this.a
10: aload_0
11: bipush 20 // <- 20
13: putfield #4 // -> this.b
16: aload_0
17: bipush 10 // <- 10
19: putfield #4 // -> this.b
22: aload_0
23: ldc #5 // <- "s2"
    
// 但是後面他們調用了原始構造方法,將傳進來的參數值作為a,b的值賦值
25: putfield #3 // -> this.a
28: aload_0 // ------------------------------
29: aload_1 // <- slot 1(a) "s3" |
30: putfield #3 // -> this.a |
33: aload_0 |
34: iload_2 // <- slot 2(b) 30 |
35: putfield #4 // -> this.b --------------------
38: return

方法調用

我們首先給出一些方法和方法調用示例:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_9 {
    
    // 構造方法
    public Demo3_9() { }

    // 私有方法
    private void test1() { }

    // 無法改變的私有方法
    private final void test2() { }

    // 公開方法
    public void test3() { }

    // 靜態公開方法
    public static void test4() { }

    // 繼承toString方法
    @Override
    public String toString() {
        return super.toString();
    }

    // 各種示例展示
    public static void main(String[] args) {
        Demo3_9 d = new Demo3_9();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        Demo3_9.test4();
        d.toString();
    }

}

我們來查看其位元組碼:

0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return

我們對其進行解釋:

  • new 是創建【對象】,給對象分配堆記憶體,執行成功會將【對象引用】壓入操作數棧

  • dup 是賦值操作數棧棧頂的內容,本例即為【對象引用】,為什麼需要兩份引用呢

  • 一個是要配合 invokespecial 調用該對象的構造方法 “<init>”😦)V (會消耗掉棧頂一個引用)

  • 另一個要配合 astore_1 賦值給局部變數

  • 最終方法(final),私有方法(private),構造方法都是由 invokespecial 指令來調用,屬於靜態綁定

  • 普通成員方法是由 invokevirtual 調用,屬於動態綁定,即支援多態

  • 成員方法與靜態方法調用的另一個區別是,執行方法前是否需要【對象引用】

  • d.test4()是通過對象引用調用一個靜態方法,但在調用invokestatic 之前執行了 pop 指令,把對象引用從操作數棧彈掉了

  • 還有一個執行 invokespecial 的情況是通過 super 調用父類方法

異常處理

我們同樣首先給出異常處理的程式碼:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_11_1 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}

我們給出重要的位元組碼部分:

{
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         // 首先這裡是try,將10賦給i
         2: bipush        10
         4: istore_1
         // 再往下是catch程式碼,我們不希望執行,所以直接採用goto跳轉到return
         5: goto          12
         // 這裡存放catch程式碼,如果檢測到錯誤,就會跳轉到這裡執行
         8: astore_2
         9: bipush        20
        11: istore_1
        12: return
      // 首先這個地方多了一個異常處理機制from和to是作用範圍,這裡是[)形式的
      // 此外target表示跳轉行數,type表示遇到什麼樣的錯誤
      // 系統會自動檢測,不需要在上述重複書寫
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception
      // Exception也會被看作對象,這裡需要進行存放
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
}

當然針對多個異常處理,我們同樣採用這種方式:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_11_2 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }

}

我們給出部分重要位元組碼:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        30
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        40
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        50
        25: istore_1
        26: return
      // 我們可以看到異常處理表中出現了其他異常的處理資訊
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
      // 同時局部變數表中也會有異常Exception的存放位置,這裡由於異常在同一處,不會同時出現,所以放在同一個局部變數即可
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
}

針對multi-catch 的情況系統的處理方法也大同小異:

package cn.itcast.jvm.t3.bytecode;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Demo3_11_3 {

    public static void main(String[] args) {
        try {
            Method test = Demo3_11_3.class.getMethod("test");
            test.invoke(null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void test() {
        System.out.println("ok");
    }
}

我們給出部分重要位元組碼:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc           #2                  // class cn/itcast/jvm/t3/bytecode/Demo3_11_3
         2: ldc           #3                  // String test
         4: iconst_0
         5: anewarray     #4                  // class java/lang/Class
         8: invokevirtual #5                  // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        11: astore_1
        12: aload_1
        13: aconst_null
        14: iconst_0
        15: anewarray     #6                  // class java/lang/Object
        18: invokevirtual #7                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        21: pop
        22: goto          30
        25: astore_1
        26: aload_1
        27: invokevirtual #11                 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
        30: return
      Exception table:
         from    to  target type
             0    22    25   Class java/lang/NoSuchMethodException
             0    22    25   Class java/lang/IllegalAccessException
             0    22    25   Class java/lang/reflect/InvocationTargetException
      // 這裡注意只標有一個坑位了,所以某種意義上是節省記憶體了~
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12      10     1  test   Ljava/lang/reflect/Method;
           26       4     1     e   Ljava/lang/ReflectiveOperationException;
            0      31     0  args   [Ljava/lang/String;
}

最後我們介紹一下finally操作:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_11_4 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}

我們查看部分重要位元組碼:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         // 前端的程式碼顯得簡單粗暴
         // 就是在try階段執行一次finally操作,直接賦值一份在catch操作裡面執行finally操作,最後再放一份finally在最後
         // 也就是說finally一共有三份,第一份在try,第二份在catch,第三份單獨存放防止前面出現異常則直接執行
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: bipush        30
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        20
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      // 重點在這裡!因為try和catch操作都有可能出現異常,所以添加了兩個異常,如果發現異常,直接跳到獨有的fianlly操作里執行
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any
		// 這裡同樣是異常佔位
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I
}

加鎖處理

最後我們介紹一下synchronized的處理問題:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_13 {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

我們同樣給出重要的位元組碼部分:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // new Object
         3: dup
         4: invokespecial #1                  // invokespecial <init>:()V
         7: astore_1						  // lock引用 -> lock
         8: aload_1							  // <- lock (synchronized開始)
         9: dup
        10: astore_2						  // lock引用 -> slot 2
        11: monitorenter					  // monitorenter(lock引用)
        12: getstatic     #3                  // <- System.out
        15: ldc           #4                  // <- "ok"
        17: invokevirtual #5                  // invokevirtual println:(Ljava/lang/String;)V
        20: aload_2							  // <- slot 2(lock引用)
        21: monitorexit						  // monitorexit(lock引用)
        22: goto          30
        25: astore_3						  // any -> slot 3
        26: aload_2							  // <- slot 2(lock引用)
        27: monitorexit						  // monitorexit(lock引用)
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;
}

編譯期處理

這一節我們將詳細介紹編譯期JVM為我們做的簡單處理

語法糖介紹

首先我們簡單介紹一下語法糖的概念:

  • 所謂的語法糖,其實就是指 java 編譯器把 *.java 源碼編譯為 *.class 位元組碼的過程中,自動生成和轉換的一些程式碼
  • 相當於在JVM的原版本的一些插件,幫助我們快速編譯

我們下面所介紹的編譯期處理基本都是語法糖的內容,我們需要注意:

  • 以下程式碼的分析,藉助了 javap 工具,idea 的反編譯功能,idea 插件 jclasslib 等工具。
  • 編譯器轉換的結果直接就是 class 位元組碼,只是為了便於閱讀,給出了等價 的 java 源碼方式

默認構造器

首先我們都知道,如果我們的類沒有書寫構造器,那麼系統會自動為我們補充一個構造器

首先這是我們的源碼:

public class Candy1 {
}

然後由編譯器在編譯期所做的處理如下:

public class Candy1 {
    // 這個無參構造是編譯器幫助我們加上的
    public Candy1() {
        super(); // 即調用父類 Object 的無參構造方法,即調用 java/lang/Object."<init>":()V
    }
}

自動拆裝箱

我們在最開始的版本中其實包是一個很普遍的概念,我們需要進行手動拆箱裝箱操作:

public class Candy2 {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}

但是在JDK5之後,系統為我們自動添加了自動拆裝箱功能,我們就可以節省掉這一步:

public class Candy2 {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}

泛型集合取值

泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型程式碼後會執行 泛型擦除 的動作,即泛型資訊

在編譯為位元組碼之後就丟失了,實際的類型都當做了 Object 類型來處理:

public class Candy3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 實際調用的是 List.add(Object e)
        Integer x = list.get(0); // 實際調用的是 Object obj = List.get(int index);
    }
}

所以實際上系統在這部分還為我們完成了一個自動類型轉換的功能:

// 需要將 Object 轉為 Integer
Integer x = (Integer)list.get(0);

如果我們前面的類型是int類型,那麼還會追加一層自動拆裝箱功能:

// 需要將 Object 轉為 Integer, 並執行拆箱操作
int x = ((Integer)list.get(0)).intValue();

可變參數

可變參數 String… args 其實是一個 String[] args ,從程式碼中的賦值語句中就可以看出來。

我們正常情況下書寫就可以將可變參數轉化為一個數組類型:

public class Candy4 {
    
    public static void foo(String... args) {
        String[] array = args; // 直接賦值
        System.out.println(array);
    } 
    
    public static void main(String[] args) {
    	foo("hello", "world");
    }
}

但是在底層中系統會為我們完成一些操作:

public class Candy4 {
    
    // 這裡接受的參數直接由args可變參數變為String類型的數組參數
    public static void foo(String[] args) {
        String[] array = args; // 直接賦值
        System.out.println(array);
    } 
    
    public static void main(String[] args) {
        // 相當於我們直接創建了String類型的數組,並且將值封裝進去,這個數組的大小就是我們傳入的大小
        foo(new String[]{"hello", "world"});
    }
}

foreach循環

我們的foreach循環操作也是由最基本的for循環來演變過來的,只不過是系統為我們進行了處理而已:

public class Candy5_1 {
    public static void main(String[] args) {
        
        // 數組賦初值的簡化寫法也是語法糖哦
        int[] array = {1, 2, 3, 4, 5}; 
        
        // 這是我們的foreach循環
        for (int e : array) {
            System.out.println(e);
        }
    }
}

下面我們來展示由系統編譯後的java程式碼:

public class Candy5_1 {
    
    public Candy5_1() {
    } 
    
    public static void main(String[] args) {
        
        // 這裡的數組賦值實際上還是調用了new int[],但是系統幫你補充,所以你可以省略
        int[] array = new int[]{1, 2, 3, 4, 5};
        
        // 這裡依舊採用的是for循環,但是系統為你封裝好了for循環的開頭與結束條件,並且幫你把數組中的元素取出來
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }
    }
}

對於集合也是同樣的概念:

public class Candy5_2 {
    public static void main(String[] args) {
        
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

我們給出集合的編譯程式碼:

public class Candy5_2 {
    
    public Candy5_2() {
    } 
    
    public static void main(String[] args) {
        
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        
        // 實際上系統我們提取了一個迭代器作為數組的遍歷條件,並為我們完成了迭代器判定以及迭代器遞增
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            Integer e = (Integer)iter.next();
            System.out.println(e);
        }
    }
}

switch字元串

我們JDK7之後的switch可以直接採用字元串來進行判定,這同樣也是底層編譯的語法糖內容:

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

我們的底層是採用hashCode和equal方法來進行判定的:

public class Candy6_1 {
    
    public Candy6_1() {
    }
        
    public static void choose(String str) {
        
        byte x = -1;
        
        // 首先進行hashCode比較,因為hashCode是唯一值,比較速度快,可以進行大規模比較
        switch(str.hashCode()) {
                
            // 然後我們再採用eq方法確定,因為有的值的hashCode是一致的,我們需要判定是否符合我們的條件 
            // 我們根據條件再設置一個x的參數用於另一個switch來執行方法
            case 99162322: // hello 的 hashCode
                if (str.equals("hello")) {
                    x = 0;
                } 
                break;
            case 113318802: // world 的 hashCode
                if (str.equals("world")) {
                    x = 1;
                }
            } 
        
            // 執行方法的switch
        	switch(x) {
                case 0:
                    System.out.println("h");
                    break;
                case 1:
                    System.out.println("w");
        }
    }
}

switch枚舉

switch和枚舉一同使用也是由系統底層進行了改造:

enum Sex {
    MALE, FEMALE
}
public class Candy7 {
    public static void foo(Sex sex) {
    switch (sex) {
        case MALE:
            System.out.println("男"); break;
        case FEMALE:
            System.out.println("女"); break;
        }
    }
}

我們底層修改如下:

public class Candy7 {
    /**
    * 定義一個合成類(僅 jvm 使用,對我們不可見)
    * 用來映射枚舉的 ordinal 與數組元素的關係
    * 枚舉的 ordinal 表示枚舉對象的序號,從 0 開始
    * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
    */
    static class $MAP {
        
        // 數組大小即為枚舉元素個數,裡面存儲case用來對比的數字
        static int[] map = new int[2];
        
        static {
                map[Sex.MALE.ordinal()] = 1;
                map[Sex.FEMALE.ordinal()] = 2;
            }
        
        } 
    
    	public static void foo(Sex sex) {
            
            int x = $MAP.map[sex.ordinal()];
            
            switch (x) {
                case 1:
                    System.out.println("男");
                break;
                case 2:
                    System.out.println("女");
                break;
            }
        }
}

枚舉類

在JDK7中甚至為我們直接設計了枚舉類:

enum Sex {
    MALE, FEMALE
}

底層直接為我們創造了一個類:

public final class Sex extends Enum<Sex> {
    
    // 首先將屬性進行定義,並定義一個VALUE數組存放這些屬性
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;
    
    // 設置一個靜態方法,將數據設置好順序號,並存入數組
    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    } 

    // 私有方法防止修改枚舉
    private Sex(String name, int ordinal) {
        super(name, ordinal);
    } 
    
    // 獲得所有枚舉數
    public static Sex[] values() {
        return $VALUES.clone();
    } 
    
    // 根據名稱獲得枚舉
    public static Sex valueOf(String name) {
        return Enum.valueOf(Sex.class, name);
    }
}

try-with-resources

JDK 7 開始新增了對需要關閉的資源處理的特殊語法try-with-resources:

try(資源變數 = 創建資源對象){
    
} catch( ) {
    
}

但是我們需要注意:

  • 其中資源對象需要實現 AutoCloseable 介面, 才能夠自動進行資源關閉
  • 例如 InputStream 、 OutputStream 、Connection 、 Statement 、 ResultSet 等介面都實現了 AutoCloseable ,

我們可以這樣書寫程式碼:

public class Candy9 {
    public static void main(String[] args) {
        // 書寫程式碼時將需要關閉的資源放在try條件中,最後系統會自動關閉
        try(InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

系統為我們所調整的程式碼如下:

public class Candy9 {
    
    public Candy9() {
    }
        
    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("d:\\1.txt");
            Throwable t = null;
            try {
                System.out.println(is);
            } catch (Throwable e1) {
                // t 是我們程式碼出現的異常
                t = e1;
                throw e1;
            } finally {
                // 判斷了資源不為空
                if (is != null) {
                    // 如果我們程式碼有異常
                    if (t != null) {
                        try {
                            is.close();
                    } catch (Throwable e2) {
                        // 如果 close 出現異常,作為被壓制異常添加
                        t.addSuppressed(e2);
                	}
            	} else {
                	// 如果我們程式碼沒有異常,close 出現的異常就是最後 catch 塊中的 e
                	is.close();
            		}
            	}
        	}
        } catch (IOException e) {
        e.printStackTrace();
        }
    }
}

方法重寫時的橋接方法

我們都知道,方法重寫時對返回值分兩種情況:

  • 父子類的返回值完全一致
  • 子類返回值可以是父類返回值的子類

我們給出一個簡單例子:

class A {
    public Number m() {
    	return 1;
    }
    }

class B extends A {
    @Override
    // 子類 m 方法的返回值是 Integer 是父類 m 方法返回值 Number 的子類
    public Integer m() {
        return 2;
    }
}

這時我們的系統也會為我們做調整:

class B extends A {
    
    public Integer m() {
        return 2;
    }
    
    // 此方法才是真正重寫了父類 public Number m() 方法
    public synthetic bridge Number m() {
        // 調用 public Integer m()
        return m();
    }
}

匿名內部類

我們平時所使用的匿名內部類其實是系統為我們自動創建的一個類:

public class Candy11 {
    public static void test(final int x) {
        
        // 例如我們這裡採用一個匿名內部類
        // 注意這裡如果為匿名內部類添加參數,需要添加final固定參數,因為我們的類中的參數是無法修改的!
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
                }
            };
    }
}

系統為我們直接創造了一個類:

// application

public class Candy11 {
    
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
    
}
// 額外生成的類
final class Candy11$1 implements Runnable {
    
    // 系統創造的屬性
    int val$x;
    
    // 第一次創造時,對屬性賦值,所以我們傳入的參數只能是不變的
    Candy11$1(int x) {
    	this.val$x = x;
    } 
    
    // 我們創造的匿名內部類的方法
	public void run() {
    	System.out.println("ok:" + this.val$x);
    }
}

類載入階段

這一節我們將詳細介紹JVM的類載入階段

載入階段

載入階段是類載入階段的第一階段

載入階段的用處是:

  • 將類的位元組碼載入方法區中,內部採用 C++ 的 instanceKlass 描述 java 類

由於我們無法直接使用C++語言的instanceKlass,所以我們需要藉助鏡像來完成載入:

  • java_mirror 即 java 的類鏡像,例如對 String 來說,就是 String.class,作用是把 klass 暴露給 java 使用

除此之外我們來介紹instanceKlass的其他重要部分:

  • _super 即父類
  • _fields 即成員變數
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即類載入器
  • _vtable 虛方法表
  • _itable 介面方法表

除此之外我們強調兩個注意點:

  • 如果這個類還有父類沒有載入,先載入父類
  • 載入和鏈接可能是交替運行的

鏈接階段

連接階段是類載入階段的第二階段

鏈接主要分為三個部分:驗證,準備,解析

驗證階段

我們簡單介紹一下驗證的含義:

  • 驗證類是否符合 JVM規範,安全性檢查

準備階段

我們簡單介紹一下解釋的含義:

  • 為 static 變數分配空間,設置默認值

其中我們需要注意:

  • static 變數在 JDK 7 之前存儲於 instanceKlass 末尾,從 JDK 7 開始,存儲於 _java_mirror 末尾
  • static 變數分配空間和賦值是兩個步驟,分配空間在準備階段完成,賦值在初始化階段完成
  • 如果 static 變數是 final 的基本類型,以及字元串常量,那麼編譯階段值就確定了,賦值在準備階段完成
  • 如果 static 變數是 final 的,但屬於引用類型,那麼賦值也會在初始化階段完成

解析階段

我們簡單介紹一下解析的含義:

  • 將常量池中的符號引用解析為直接引用
  • 這個階段對我們程式設計師來說幾乎是透明的,不需要過多了解

初始化階段

首先我們先來簡單介紹一下<cinit>方法:

  • 即static方法,會將所有的賦值按順序堆積在static方法中,在第一次構造類時執行
  • 初始化即調用<cinit>()V ,虛擬機會保證這個類的『構造方法』的執行緒安全

初始化時機:

  • main 方法所在的類,總會被首先初始化
  • 首次訪問這個類的靜態變數或靜態方法時
  • 子類初始化,如果父類還沒初始化,會引發
  • 子類訪問父類的靜態變數,只會觸發父類的初始化
  • Class.forName
  • new 會導致初始化

不會初始化的時間點:

  • 訪問類的 static final 靜態常量(基本類型和字元串)不會觸發初始化
  • 類對象.class 不會觸發初始化
  • 創建該類的數組不會觸發初始化
  • 類載入器的 loadClass 方法
  • Class.forName 的參數 2 為 false 時

我們給出簡單驗證的程式碼:

package cn.itcast.jvm.t3.load;

import java.io.IOException;

public class Load3 {
    
    static {
        System.out.println("main init");
    }
    
    // 每次選擇一個測試~
    public static void main(String[] args) throws ClassNotFoundException, IOException {
//        // 1. 靜態常量不會觸發初始化
//        System.out.println(B.b);
//        // 2. 類對象.class 不會觸發初始化
//        System.out.println(B.class);
//        // 3. 創建該類的數組不會觸發初始化
//        System.out.println(new B[0]);
//        // 4. 不會初始化類 B,但會載入 B、A
//        ClassLoader cl = Thread.currentThread().getContextClassLoader();
//        cl.loadClass("cn.itcast.jvm.t3.load.B");
//        // 5. 不會初始化類 B,但會載入 B、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
        System.in.read();


//        // 1. 首次訪問這個類的靜態變數或靜態方法時
//        System.out.println(A.a);
//        // 2. 子類初始化,如果父類還沒初始化,會引發
//        System.out.println(B.c);
//        // 3. 子類訪問父類靜態變數,只觸發父類初始化
//        System.out.println(B.a);
//        // 4. 會初始化類 B,並先初始化類 A
//        Class.forName("cn.itcast.jvm.t3.load.B");
    }
}

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

類載入器

這一節我們將詳細介紹JVM的類載入器

四種基本類載入器

我們先來介紹四種基本類載入器:

名稱 載入哪的類 說明
Bootstrap ClassLoader JAVA_HOME/jre/lib 無法直接訪問
Extension ClassLoader JAVA_HOME/jre/lib/ext 上級為 Bootstrap,顯示為 null
Application ClassLoader classpath 上級為 Extension
自定義類載入器 自定義 上級為 Application

我們簡單介紹一下運行機制:

  • 首先我們需要知道Bootstrap ClassLoader 是不可訪問的,當我們查找到該層級時會顯示null
  • 我們如果需要載入一個類,會先檢測是否有上級,如果有上級就到上級中去,如果沒有就在本層查找是否有該類
  • 意思就是以最高級的載入器中的類為最高標準,如果同時存在多個類,我們會選擇最高級的類載入器中的類來運行

啟動類載入器

用 Bootstrap 類載入器載入類:

package cn.itcast.jvm.t3.load;

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

執行 :

package cn.itcast.jvm.t3.load;

public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }
}

輸出:

// 我們會發現最後輸出的結果為null,這就是啟動類載入器,因為我們無法使用,所以JVM索性給我們定義一個null返回

// 運行~
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
    
// 運行結果
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

我們對上述的運行語句做簡單解釋:

  • -Xbootclasspath 表示設置 bootclasspath
  • 其中 /a:. 表示將當前目錄追加至 bootclasspath 之後

擴展類載入器

用Extension ClassLoader 類載入器載入類:

package cn.itcast.jvm.t3.load;

public class G {
    static {
//        System.out.println("ext G init");
        System.out.println("classpath G init");
    }
}

執行:

package cn.itcast.jvm.t3.load;

/**
 * 演示 擴展類載入器
 * 在 C:\Program Files\Java\jdk1.8.0_91 下有一個 my.jar
 * 裡面也有一個 G 的類,觀察到底是哪個類被載入了
 */
public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

輸出:

// 目前是由Application ClassLoader 載入器運行的!(其實我們目前使用的基本都是Application類載入器)
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

但是如果我們將該程式打成jar包之後並放在Extension ClassLoader的對應目錄下之後我們再來運行:

// 我們就會發現類載入器目錄變為了Extension ClassLoader
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

雙親委派模式

首先我們來簡單介紹一下雙親委派模式:

  • 所謂的雙親委派,就是指調用類載入器的 loadClass 方法時,查找類的規則
  • 意思就是我們要遵從雙親(上級)的指令,以雙親為主,先調用雙親的類引用,再考慮自身

我們給出雙親委派模式的程式碼:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1. 檢查該類是否已經載入
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2. 有上級的話,委派上級 loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        // 3. 如果沒有上級了(ExtClassLoader),則委派BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
					//	就算出現異常,這裡捕獲不做任何處理,繼續運行程式
                }

                if (c == null) {
                    // 4. 每一層找不到,調用 findClass 方法(每個類載入器自己擴展)來載入
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // 5. 記錄耗時
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

自定義類載入器

首先我們簡單介紹一下自定義類載入器:

  • 由我們自己設置什麼情況下在什麼文件夾下取得類的本體

那麼我們給出我們需要自定義類載入器的情況:

  • 想載入非 classpath 隨意路徑中的類文件
  • 都是通過介面來使用實現,希望解耦時,常用在框架設計
  • 這些類希望予以隔離,不同應用的同名類都可以載入,不衝突,常見於 tomcat 容器

我們這裡給出簡單步驟:

  • 繼承 ClassLoader 父類
  • 要遵從雙親委派機制,重寫 findClass 方法(注意不是重寫 loadClass 方法,否則不會走雙親委派機制)
  • 讀取類文件的位元組碼
  • 調用父類的 defineClass 方法來載入類
  • 使用者調用該類載入器的 loadClass 方法

運行期優化

這一節我們將詳細介紹JVM的運行期優化

即時編譯

首先我們來簡單介紹一下即時編譯:

  • 即時編譯就是在編譯的過程中調整編譯的細節狀態,例如內部引用內部構造等

我們給出一個簡單的例子:

package cn.itcast.jvm.t3.jit;

public class JIT1 {

    // -XX:+PrintCompilation -XX:-DoEscapeAnalysis
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }
}

我們給出運行階段的部分內容:

// 前面是第幾次,後面是具體時間

// 我們會發現隨著次數的增多,我們的運行時間越來越少,這其實就是即時編譯的功勞

// 第一階段:非常大~
0 96426
1 52907
2 44800
3 119040
4 65280
5 47360
6 45226
7 47786
8 48640
9 60586
10 42667

// 第二階段:相對減少~
82 18774
83 17067
84 21760
85 23467
86 17920
87 17920
88 18774
89 18773
90 19200

// 第三階段:非常少~
190 1280
191 853
192 853
193 853
194 853
195 854
196 853
197 853
198 853
199 854

我們首先來解釋一個概念:

  • profiling 是指在運行過程中收集一些程式執行狀態的數據,例如【方法的調用次數】,【循環的回邊次數】等

其實我們的即時編譯會被分為五個時間段:

  • 0 層,解釋執行(Interpreter)
  • 1 層,使用 C1 即時編譯器編譯執行(不帶 profiling)
  • 2 層,使用 C1 即時編譯器編譯執行(帶基本的 profiling)
  • 3 層,使用 C1 即時編譯器編譯執行(帶完全的 profiling)
  • 4 層,使用 C2 即時編譯器編譯執行

那麼我們就來介紹即時編譯和解釋器的區別:

  • 解釋器是將位元組碼解釋為機器碼,下次即使遇到相同的位元組碼,仍會執行重複的解釋
  • JIT 是將一些位元組碼編譯為機器碼,並存入 Code Cache,下次遇到相同的程式碼,直接執行,無需再編譯
  • 解釋器是將位元組碼解釋為針對所有平台都通用的機器碼
  • JIT 會根據平台類型,生成平台特定的機器碼

可是即時編譯也會有部分缺陷,我們也不能將所有程式碼都使用即時編譯來進行:

  • 對於佔據大部分的不常用的程式碼,我們無需耗費時間將其編譯成機器碼,而是採取解釋執行的方式運行;
  • 另一方面,對於僅佔據小部分的熱點程式碼,我們則可以將其編譯成機器碼,以達到理想的運行速度。

我們將這種思想稱為逃逸分析:

  • 逃逸分析就是判斷新建的對象是否逃逸。
  • 可以使用 -XX:-DoEscapeAnalysis 關閉逃逸分析

方法內聯

我們來簡單介紹一下方法內聯:

  • 當方法的篇幅較短時,我們會直接將方法的內容替換到引用方法的位置

我們舉一個簡單的例子:

// 對於這麼一個簡單的返回方法
private static int square(final int i) {
	return i * i;
}

// 如果我們調用下述語句:
System.out.println(square(9));

// 在編譯器中就會這樣處理:
System.out.println(9 * 9);

// 甚至還可以做常量摺疊的優化:
System.out.println(81);

欄位優化

我們簡單介紹一下欄位優化:

  • 欄位優化就是將之前所使用的欄位存儲在一個我們所看不到的默認數組中,我們再次調用或其他操作時會直接在數組中獲取

我們給出簡單例子:

package test;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {

    // 簡單來說就是,我們會調用下述三個方法1000次,我們會判斷其執行順序
    
    // 我們不做示例直接給出結果:三者基本一致,因為他們都藉助了數組來儲存數據
    
    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    // 這是我們平時會書寫的程式碼,但是系統在for之前為我們做了一個數組,用來存放elements的值,就和test2一樣
    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    // 這是標準格式
    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    // 這是foreeach循環,我們在前面通過位元組碼分析可以知道foreach是完全基於for循環製作的,所以他也存在數組存放數據
    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }

    static int sum = 0;

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    static void doSum(int x) {
        sum += x;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

反射優化

反射優化也是類似於即時編譯的優化方法:

  • 在我們的運行期間會存在一個閾值
  • 在運行次數未到達閾值之前,我們會採用反射提供的方法或者類來運行
  • 但是當運行次數到達閾值之後,我們就會視情況採用我們運行過程中提供的方法或者類來進行餘下的運行

我們給出簡單示例:

package cn.itcast.jvm.t3.reflect;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflect1 {

    public static void foo() {
        System.out.println("foo...");
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

我們來介紹示例中的運行結果:

  • 我們在上述的前十六次運行中會發現時間慢慢遞減,這是因為我們的部分存儲導致的

  • 但是我們的第十七次運行就會明顯發現時間大幅度減少,這是因為我們不再使用反射的途徑獲得foo,而是直接使用方法中的foo

結束語

到這裡我們JVM的類載入和位元組碼技術篇就結束了,希望能為你帶來幫助~

附錄

該文章屬於學習內容,具體參考B站黑馬程式設計師滿老師的JVM完整教程

這裡附上影片鏈接:01-類載入-概述_嗶哩嗶哩_bilibili