天天看點

JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化

JVM-類加載與位元組碼技術

  • 類結構檔案
    • magic
    • 版本
    • 常量池
  • javap工具
    • 圖解方法執行流程
    • 條件判斷指令
    • 構造方法
      • cinit 構造方法
      • init 構造方法
    • 方法調用
    • 多态的原理
    • synchronized
  • 編譯期處理
    • 預設構造器
    • 自動拆裝箱
    • 泛型集合取值
    • 可變參數
    • foreach 循環
      • 數組循環
      • 集合循環
    • switch 字元串
    • try-with-resources
    • 方法重寫時的橋接方法
    • 匿名内部類
      • 無引用局部變量
      • 引用局部變量
  • 類加載階段
    • 加載
    • 連結
      • 驗證
      • 準備
      • 解析
    • 初始化
      • 發生的時機
  • 運作期優化
    • 及時編譯
      • 分層編譯
      • 方法内聯
      • 字段優化
    • 反射優化
JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化

類結構檔案

  • java程式
// HelloWorld 示例
public class HelloWorld {
	public static void main(String[] args) {
	System.out.println("hello world");
	}
}
           
  • 執行 javac -parameters -d . HellowWorld.java,編譯為 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
           
  • 根據 JVM 規範,類檔案結構如下
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];
}
           

magic

  • 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

常量池

JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • 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
  • 第#1項 0a 表示一個 Method 資訊,查上述表發發現是一個方法引用的資訊。CONSTANT_Methodref
    • 00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 項來獲得這個方法的【所屬類】和【方法名】。

省略

javap工具

  • 自己分析類檔案結構太麻煩了,Oracle 提供了 javap 工具來反編譯 class 檔案

圖解方法執行流程

  • 原始java檔案
/**
* 示範 位元組碼指令 和 操作數棧、常量池的關系
*/
public class Demo3_1 {
public static void main(String[] args) {
	int a = 10;   //比較小的數值,例如10,并不是存儲在常量池中,而是和方法的位元組碼指令存在一起
	int b = Short.MAX_VALUE + 1; //數值的範圍超過了整數的最大值,會存儲在常量池中
	int c = a + b;
	System.out.println(c);
	}
}
           
  • 位元組碼檔案
Classfile /E:/studyData/JVM/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2021-3-11; 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
}
           
  1. 常量池載入運作時常量池
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  2. 方法位元組碼載入方法區
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  3. main 線程開始運作,配置設定棧幀記憶體
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • 藍色:操作數棧;綠色:局部變量表。在位元組碼檔案中,定義了其大小(stack=2,locals=4)
  1. 執行引擎開始執行位元組碼
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • bipush 10:将一個 byte 壓入操作數棧(其長度會補齊 4 個位元組),類似的指令還有
    • sipush 将一個 short 壓入操作數棧(其長度會補齊 4 個位元組)
    • ldc 将一個 int 壓入操作數棧
    • ldc2_w 将一個 long 壓入操作數棧(分兩次壓入,因為 long 是 8 個位元組)
    • 這裡小的數字都是和位元組碼指令存在一起,超過 short 範圍的數字存入了常量池
  • istore_1:将操作數棧頂資料彈出,存入局部變量表的 slot 1
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • ldc #3:從常量池加載 #3 資料到操作數棧
    • 注意 Short.MAX_VALUE 是 32767,是以 32768 = Short.MAX_VALUE + 1 實際是在編譯期間計算好的。
      JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • getstatic #4
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • invokevirtual #5
    • 找到常量池 #5 項
    • 定位到方法區 java/io/PrintStream.println:(I)V 方法
    • 生成新的棧幀(配置設定 locals、stack等)
    • 傳遞參數,執行新棧幀中的位元組碼。
    • 注意:傳遞參數實際上是将參數傳遞到println棧幀的操作數棧中。然後在這個棧幀中進行調用。
  • JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • 執行完畢,彈出棧幀
    • 清除 main 操作數棧内容
      JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • return
    • 完成 main 方法調用,彈出 main 棧幀
    • 程式結束

條件判斷指令

JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
  • 幾點說明:
    • byte,short,char 都會按 int 比較,因為操作數棧都是 4 位元組
    • goto 用來進行跳轉到指定行号的位元組碼
    • 以上比較指令中沒有 long,float,double 的比較,那麼它們要比較怎麼辦?參考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

構造方法

cinit 構造方法

  • java
public class Demo3_8_1 {
	static int i = 10;
	static {
		i = 20;
	}
	static {
		i = 30;
	}
}
           
  • 編譯器會按從上至下的順序,收集所有 static 靜态代碼塊和靜态成員指派的代碼,合并為一個特殊的方法 < cinit >()V
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化

init 構造方法

  • java
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);
    }
}
           
  • 編譯器會按從上至下的順序,收集所有 {} 代碼塊和成員變量指派的代碼,形成新的構造方法,但原始構造方法内的代碼總是在最後。
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化

方法調用

  • java
public class Demo3_9 {
    public Demo3_9() { }

    private void test1() { }

    private final void test2() { }

    public void test3() { }

    public static void test4() { }

    @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();
    }
}
           
  • 位元組碼
    JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化
    • new 是建立【對象】,給對象配置設定堆記憶體,執行成功會将【對象引用】壓入操作數棧
    • dup 是指派操作數棧棧頂的内容,本例即為【對象引用】。為什麼需要兩份引用呢,一個是要配合 invokespecial 調用該對象的構造方法 “”😦)V (會消耗掉棧頂一個引用),另一個要配合 astore_1 指派給局部變量
    • 最終方法(final),私有方法(private),構造方法都是由 invokespecial 指令來調用,屬于靜态綁定。在編譯的時候就确定的知道調用的哪個方法,直接可找到代碼的執行位址。
    • 普通成員方法是由 invokevirtual 調用,屬于動态綁定,在運作的時候才知道調用的哪個方法。即支援多态成員方法與靜态方法調用的另一個差別是,執行方法前是否需要【對象引用】
    • 比較有意思的是 d.test4(); 是通過【對象引用】調用一個靜态方法,可以看到在調用invokestatic (也屬于靜态綁定)之前執行了 pop 指令,把【對象引用】從操作數棧彈掉了。因為靜态方法不需要對象,是通過類名。
    • 還有一個執行 invokespecial 的情況是通過 super 調用父類方法

多态的原理

  • java
/**
 * 示範多态原理,注意加上下面的 JVM 參數,禁用指針壓縮。因為64位虛拟機為了節省記憶體空間,使用了指針壓縮技術,檢視記憶體位址時需要進行位址變換。
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointe	rs
 */
public class Demo3_10 {

    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }

    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }
}

abstract class Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}

class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("啃骨頭");
    }
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("吃魚");
    }
}

           
  • 當執行 invokevirtual 指令時,
  1. 先通過棧幀中的對象引用找到對象
  2. 分析對象頭,找到對象的實際 Class
  3. Class 結構中有 vtable,它在類加載的連結階段就已經根據方法的重寫規則生成好了
  4. 查表得到方法的具體位址
  5. 執行方法的位元組碼

synchronized

  • java
public class Demo3_13 {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
           
  • 位元組碼檔案
Classfile /E:/studyData/JVM/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_13.class
  Last modified 2021-3-11; size 735 bytes
  MD5 checksum 2f9da4c55f7511811c01df8263d5fe33
  Compiled from "Demo3_13.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_13
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#26         // java/lang/Object."<init>":()V
   #2 = Class              #27            // java/lang/Object
   #3 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #30            // ok
   #5 = Methodref          #31.#32        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #33            // cn/itcast/jvm/t3/bytecode/Demo3_13
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_13;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               lock
  #19 = Utf8               Ljava/lang/Object;
  #20 = Utf8               StackMapTable
  #21 = Class              #17            // "[Ljava/lang/String;"
  #22 = Class              #27            // java/lang/Object
  #23 = Class              #34            // java/lang/Throwable
  #24 = Utf8               SourceFile
  #25 = Utf8               Demo3_13.java
  #26 = NameAndType        #7:#8          // "<init>":()V
  #27 = Utf8               java/lang/Object
  #28 = Class              #35            // java/lang/System
  #29 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #30 = Utf8               ok
  #31 = Class              #38            // java/io/PrintStream
  #32 = NameAndType        #39:#40        // println:(Ljava/lang/String;)V
  #33 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_13
  #34 = Utf8               java/lang/Throwable
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (Ljava/lang/String;)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_13();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_13;

  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                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 12
        line 9: 20
        line 10: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 25
          locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "Demo3_13.java"
           
方法級别的 synchronized 不會在位元組碼指令中有所展現
           

編譯期處理

  • 所謂的 文法糖,其實就是指 java 編譯器把 *.java 源碼編譯為 *.class 位元組碼的過程中,自動生成和轉換的一些代碼,主要是為了減輕程式員的負擔,算是 java 編譯器給我們的一個額外福利。

    注意,以下代碼的分析,借助了 javap 工具,idea 的反編譯功能,idea 插件 jclasslib 等工具。另外,編譯器轉換的結果直接就是 class 位元組碼,隻是為了便于閱讀,給出了 幾乎等價 的 java 源碼方式,并不是編譯器還會轉換出中間的 java 源碼,切記。

預設構造器

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

自動拆裝箱

  • 這個特性是 JDK 5 開始加入的。
public class Candy2 {
	public static void main(String[] args) {
		Integer x = 1;
		int y = x;
	}
}
           
  • 上述代碼在 JDK 5 之前是無法編譯通過的,必須改寫
public class Candy2 {
	public static void main(String[] args) {
		Integer x = Integer.valueOf(1);
		int y = x.intValue();
	}
}
           

泛型集合取值

  • 泛型也是在 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);
           
  • 如果前面的 x 變量類型修改為 int 基本類型那麼最終生成的位元組碼是:
// 需要将 Object 轉為 Integer, 并執行拆箱操作
int x = ((Integer)list.get(0)).intValue();
           

可變參數

  • 可變參數也是 JDK 5 開始加入的新特性
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");
	}
}
           
  • 可變參數 String… args 其實是一個 String[] args ,從代碼中的指派語句中就可以看出來。 同樣 java 編譯器會在編譯期間将上述代碼變換為:
public class Candy4 {
	public static void foo(String[] args) {
		String[] array = args; // 直接指派
		System.out.println(array);
	}
	public static void main(String[] args) {
		foo(new String[]{"hello", "world"});
	}
}
           
注意: 如果調用了 foo() 則等價代碼為 foo(new String[]{}) ,建立了一個空的數組,而不會傳遞 null 進去
           

foreach 循環

數組循環

  • 仍是 JDK 5 開始引入的文法糖,數組的循環:
public class Candy5_1 {
	public static void main(String[] args) {
		int[] array = {1, 2, 3, 4, 5}; // 數組賦初值的簡化寫法也是文法糖哦
		for (int e : array) {
			System.out.println(e);
		}
	}
}
           
  • 會被編譯器轉換為
public class Candy5_1 {
	public Candy5_1() {
	}
	public static void main(String[] args) {
		int[] array = new int[]{1, 2, 3, 4, 5};
		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 字元串

  • 從 JDK 7 開始,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;
			}
		}
	}
}
           
注意 switch 配合 String 和枚舉使用時,變量不能為null
           
  • 會被編譯器轉換為:
public class Candy6_1 {
	public Candy6_1() {
	}
	public static void choose(String str) {
		byte x = -1;
		switch(str.hashCode()) {
			case 99162322: // hello 的 hashCode
				if (str.equals("hello")) {
				x = 0;
				}
			break;
			case 113318802: // world 的 hashCode
				if (str.equals("world")) {
					x = 1;
				}
			}
		switch(x) {
			case 0:
				System.out.println("h");
				break;
			case 1:
				System.out.println("w");
		}
	}
}
           
  • 可以看到,執行了兩遍 switch,第一遍是根據字元串的 hashCode 和 equals 将字元串的轉換為相應byte 類型,第二遍才是利用 byte 執行進行比較。
  • 為什麼第一遍時必須既比較 hashCode,又利用 equals 比較呢?hashCode 是為了提高效率,減少可能的比較;而 equals 是為了防止 hashCode 沖突,例如 BM 和 C. 這兩個字元串的hashCode值都是2123 ,如果有如下代碼:
public class Candy6_2 {
	public static void choose(String str) {
		switch (str) {
			case "BM": {
			System.out.println("h");
			break;
			}

			case "C.": {
			System.out.println("w");
			break;
			}
		}
	}
}
           
  • 會被編譯器轉換為:
public class Candy6_2 {
	public Candy6_2() {
	}
	public static void choose(String str) {
		byte x = -1;
		switch(str.hashCode()) {
		case 2123: // hashCode 值可能相同,需要進一步用 equals 比較
			if (str.equals("C.")) {
				x = 1;
			} else if (str.equals("BM")) {
				x = 0;
			}
		default:
			switch(x) {
				case 0:
					System.out.println("h");
					break;
				case 1:
					System.out.println("w");
			}
		}
	}
}
           

try-with-resources

  • JDK 7 開始新增了對需要關閉的資源處理的特殊文法try-with-resources
try(資源變量 = 建立資源對象){
} catch( ) {
}
           
  • 其中資源對象需要實作 AutoCloseable 接口,例如 InputStream 、OutputStream 、Connection 、Statement 、ResultSet 等接口都實作了 AutoCloseable ,使用 try-withresources可以不用寫 finally 語句塊,編譯器會幫助生成關閉資源代碼,例如:
public class Candy9 {
	public static void main(String[] args) {
		try(InputStream is = new FileInputStream("d:\\1.txt")) {
			System.out.println(is);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
           
  • 編譯器轉換後的代碼會捕獲異常,通過finally進行資源的關閉

方法重寫時的橋接方法

  • 方法重寫時對傳回值分兩種情況:
    • 父子類的傳回值完全一緻
    • 子類傳回值可以是父類傳回值的子類,詳細看例子。
class A {
	public Number m() {
		return 1;
	}
}
class B extends A {
	@Override
	// 子類 m 方法的傳回值是 Integer 是父類 m 方法傳回值 Number 的子類
	public Integer m() {
		return 2;
	}
}
           
  • 對子子類,java編譯器會做如下處理。生成橋接方法會調用原始的方法。
class B extends A {
	public Integer m() {
		return 2;
	}
	// 此方法才是真正重寫了父類 public Number m() 方法
	public synthetic bridge Number m() {
		// 調用 public Integer m()
		return m();
	}
}
           
  • 其中橋接方法比較特殊,僅對 java 虛拟機可見,并且與原來的 public Integer m() 沒有命名沖突

匿名内部類

無引用局部變量

  • java
public static void main(String[] args) {
	Runnable runnable = new Runnable() {
		@Override
		public void run() {
			System.out.println("ok");
		}
	};
}
           
  • 轉換後代碼:
// 額外生成的類
final class Candy11$1 implements Runnable {
	Candy11$1() {
	}
	public void run() {
		System.out.println("ok");
	}
}

public class Candy11 {
	public static void main(String[] args) {
		Runnable runnable = new Candy11$1();
	}
}
           

引用局部變量

  • java
public class Candy11 {
	public static void test(final int x) {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("ok:" + 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);
	}
}


public class Candy11 {
	public static void test(final int x) {
		Runnable runnable = new Candy11$1(x);
	}
}
           
這同時解釋了為什麼匿名内部類引用局部變量時,局部變量必須是 final 的。因為當x發生改變時,會導緻原始類中局部變量會跟額外生成的類中的屬性不一緻。
 因為在建立Candy11$1 對象時,将 x 的值指派給了 Candy11$1 對象的 val&$x屬性所有x不應該再發生變化。
 如果變化,那麼val$x屬性也不會再跟着一起變化。 
           

類加載階段

加載

  • 将類的位元組碼載入方法區中,内部采用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
    • _java_mirror 即 java 的類鏡像。例如對 String 來說,類鏡像就是 String.class,作用是把 klass 暴露給 java 使用。可以了解為是C++與java之間的橋梁。
    • _super 即父類
    • _fields 即成員變量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即類加載器
    • _vtable 虛方法表
    • _itable 接口方法表
  • 如果這個類還有父類沒有加載,先加載父類
  • 加載和連結可能是交替運作的
  • 注意:
    • instanceKlass 這樣的【中繼資料】是存儲在方法區(1.8 後的元空間内),但 _java_mirror是存儲在堆中
    • 可以通過前面介紹的 HSDB 工具檢視
      JVM-類加載與位元組碼技術類結構檔案javap工具編譯期處理類加載階段運作期優化

連結

驗證

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

準備

  • 為 static 變量配置設定空間,設定預設值
    • static 變量在 JDK 7 之前存儲于 instanceKlass 末尾,也就是存儲在方法區中。從 JDK 7 開始,存儲于_java_mirror 末尾,也就是存儲在堆中。
    • static 變量配置設定空間和指派是兩個步驟,配置設定空間在準備階段完成,指派在初始化階段完成
    • 如果 static 變量是 final 的基本類型,以及字元串常量,那麼編譯階段值就确定了,指派在準備階段完成
    • 如果 static 變量是 final 的,但屬于引用類型,那麼指派也會在初始化階段完成
  • 注意事項:如上所示,靜态變量在JDK7之前存儲在方法區,在這之後存儲在堆中。某些教材并沒有做差別。

解析

  • 将常量池中的符号引用解析為直接引用。
  • 符号引用:并不知道這符号到底對應的哪個記憶體位址,僅僅隻是符号而已。
/**
 * 解析的含義
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
       	ClassLoader classloader = Load2.class.getClassLoader();
        //隻涉及類的加載,不會觸發解析以及初始化
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        //new C();    //會觸發C的加載,解析,初始化過程。是以會加載類D
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {

}
           

初始化

< cinit >()V 方法:初始化即調用 < cinit >()V ,虛拟機會保證這個類的『構造方法』的線程安全

發生的時機

  • 概括得說,類初始化是【懶惰的】
    • main 方法所在的類,總會被首先初始化
    • 首次通路這個類的靜态變量或靜态方法時
    • 子類初始化時,如果父類還沒初始化,會觸發父類的初始化。
    • 子類通路父類的靜态變量,隻會觸發父類的初始化
    • 執行Class.forName時,會導緻類的初始化。執行過程:JVM會先檢查Class對象是否裝入記憶體,如果沒有裝入記憶體,則将Class對象裝入記憶體,然後傳回Class對象,如果裝入記憶體,則直接傳回Class對象。在加載Class對象後,會對類進行初始化,即執行類的靜态代碼塊。
    • new 會導緻初始化
  • 不會導緻類初始化的情況
    • 通路類的 static final **靜态常量(基本類型和字元串)**不會觸發初始化
    • 類對象.class 不會觸發初始化。執行過程:執行類名.class時,JVM會先檢查Class對象是否裝入記憶體,如果沒有裝入記憶體,則将Class對象裝入記憶體,然後傳回Class對象,如果裝入記憶體,則直接傳回Class對象。在加載Class對象後,不會對Class對象進行初始化。
    • 建立該類的數組不會觸發初始化
    • 類加載器的loadClass方法
    • Class.forName的參數2為false
  • 練習:懶惰初始化的單例模式
    • 實作特點是:
      • 懶惰執行個體化
      • 初始化時的線程安全是有保障的
public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();
        Singleton.getInstance();
    }

}

class Singleton {

    public static void test() {
        System.out.println("test");
    }
	//設為私有的,隻有自己才能使用構造方法
    private Singleton() {}
	//懶惰模式,使用時才建立,而不是提前建立好
    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }

    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

           

運作期優化

及時編譯

分層編譯

  • 代碼示範:從運作結果可以看出,循環建立對象後,消耗的時間變小。
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));
        }
    }
}
           

原因解析:

  • JVM 将執行狀态分成了 5 個層次:
    • 0 層,依靠解釋器來解釋執行(Interpreter)
    • 1 層,使用 C1 即時編譯器編譯執行(不帶 profiling)。當位元組碼被反複調用到達一定的門檻值後,啟用編譯器來對位元組碼進行編譯執行,
    • 2 層,使用 C1 即時編譯器編譯執行(帶基本的 profiling)
    • 3 層,使用 C1 即時編譯器編譯執行(帶完全的 profiling)
    • 4 層,使用 C2 即時編譯器編譯執行
profiling 是指在運作過程中收集一些程式執行狀态的資料,例如【方法的調用次數】,【循環的回邊次數】等
  • 即時編譯器(JIT)與解釋器的差別
    • 解釋器是将位元組碼解釋為機器碼,下次即使遇到相同的位元組碼,仍會執行重複的解釋
    • JIT 是将一些位元組碼編譯為機器碼,并存入 Code Cache,下次遇到相同的代碼,直接執行,無需再編譯
    • 解釋器是将位元組碼解釋為針對所有平台都通用的機器碼
    • JIT 會根據平台類型,生成平台特定的機器碼
  • JVM采用的政策:對于占據大部分的不常用的代碼,無需耗費時間将其編譯成機器碼,而是采取解釋執行的方式運作;另一方面,對于僅占據小部分的熱點代碼,我們則可以将其編譯成機器碼,以達到理想的運作速度。 執行效率上簡單比較一下 Interpreter < C1 < C2,總的目标是發現熱點代碼(hotspot名稱的由來),優化之。在上述的示例中,所使用的優化手段為

    逃逸分析

    ,需要重點了解。

方法内聯

  • 重點了解

字段優化

  • 即時編譯器會優化執行個體字段和靜态字段的通路,以減少總的記憶體通路次數
  • 即時編譯器将沿着控制流,緩存各個字段存儲節點将要存儲的值,或者字段讀取節點所得到的值
    • 當即時編譯器遇到對同一字段的讀取節點時,如果緩存值還沒有失效,那麼将讀取節點替換為該緩存值
    • 當即時編譯器遇到對同一字段的存儲節點時,會更新所緩存的值
      • 當即時編譯器遇到可能更新字段的節點時,它會采取保守的政策,舍棄所有的緩存值
      • 方法調用節點:在即時編譯器看來,方法調用會執行未知代碼
      • 記憶體屏障節點:其他線程可能異步更新了字段

反射優化

  • java
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.invoke 前面 0 ~ 15 次調用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 實作,為本地方法,執行費時。
  • 預設執行次數大于15後,會生成一個新的方法通路器類,代替掉最初的實作。