JVM相關(guān) : 3. 類加載和字節(jié)碼

1. 類文件結(jié)構(gòu)

根據(jù)jvm規(guī)范,類文件結(jié)構(gòu)如下:

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];
}

1.1 魔數(shù)

0~3 字節(jié),表示它是否是 class 類型的文件

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

1.2 版本

4~7字節(jié),表示類的版本 (major version)00 34 (52),表示java8

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

1.3 常量池

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

8~9字節(jié),表示常量池長度,00 23(35)表示常量池有 #1~#34 項(xiàng),注意 #0 項(xiàng)不計(jì)入,也沒有值

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

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

0000000 ca fe ba be 00 00 00 34 00 230a 00 06 00 15 09

第#2項(xiàng) 09 表示一個(gè)Field信息,00 16 (22) 和 00 17(23)表示它引用了常量池中的 #22 #23項(xiàng)來獲得這個(gè)成員變量的【所屬類】和 【成員變量名】

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

......

2. 字節(jié)碼指令

構(gòu)造方法的字節(jié)碼指令

2a b7 00 01 b1

2a -> aload_0

  • 加載 slot0的局部變量,即this,作為下面的invokespecial 構(gòu)造方法調(diào)用的參數(shù)

b7->invokespecial

  • 預(yù)備調(diào)用構(gòu)造方法,哪個(gè)方法呢?
    00 01 表示引用常量池中的第#1項(xiàng),即【Method java/lang/Object."<init>":()V】

b1->return

  • 表示返回

主方法main的字節(jié)碼指令

b2 00 02 12 03 b6 00 04 b1

b2->getstatic

  • 用來加載靜態(tài)變量,哪個(gè)靜態(tài)變量呢?
    00 02 引用常量池中的第#2項(xiàng),即【Field java/lang/System:out:Ljava/io/PrintStream;】

12->ldc

  • 加載參數(shù)
    03 引用常量池中的第#3項(xiàng),即【String hello world】

b6 ->invokevirtual

  • 預(yù)備調(diào)用成員方法,哪個(gè)方法呢?
    00 04 引用常量池的第#4項(xiàng),即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】

b1->return

  • 表示返回

2.1 javap 工具

javap -v HelloWorld.class

Classfile /C:/D/code/juc/target/classes/com/lily/jvm/HelloWorld.class
  Last modified 2021-9-13; size 559 bytes
  MD5 checksum edfc1b40953449a97c3a448c0d9f6620
  Compiled from "HelloWorld.java"
public class com.lily.jvm.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/lily/jvm/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/lily/jvm/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/lily/jvm/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.lily.jvm.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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lily/jvm/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 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

2.2 方法執(zhí)行流程

2.2.1 原始Java 代碼

public class Test {

    public static void main(String[] args) {
        int a = 10;
        //short 范圍內(nèi)的數(shù)字跟字節(jié)碼指令存儲(chǔ)在一起,大于的數(shù)字存儲(chǔ)在常量池
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

2.2.2 編譯后的字節(jié)碼文件

public class com.lily.jvm.Test
  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            // com/lily/jvm/Test
   #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               Lcom/lily/jvm/Test;
  #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               Test.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               com/lily/jvm/Test
  #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 com.lily.jvm.Test();
    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   Lcom/lily/jvm/Test;

  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 6: 0
        line 7: 3
        line 8: 6
        line 9: 10
        line 10: 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
}

2.2.3 執(zhí)行流程

1.常量池載入運(yùn)行時(shí)常量池
2.方法字節(jié)碼載入方法區(qū)
3.main 線程開始運(yùn)行,分配棧幀內(nèi)存,操作數(shù)棧和本地變量表(stack = 2, locals = 4)
4.執(zhí)行引擎開始執(zhí)行字節(jié)碼
bipush 10

  • 將一個(gè)byte壓入操作數(shù)棧,其長度會(huì)補(bǔ)齊四個(gè)字節(jié)
    類似指令有:
    sipush 將一個(gè)short壓入操作數(shù)棧,其長度會(huì)補(bǔ)齊四個(gè)字節(jié)
    ldc 將一個(gè)Int 壓入操作數(shù)棧
    ldc2_w將一個(gè)long 壓入操作數(shù)棧,分兩次壓入 ,long是8個(gè)字節(jié)
    小的數(shù)字都和字節(jié)碼指令在一起,超過short范圍的數(shù)字存入常量池

istore_1

  • 將操作數(shù)棧頂數(shù)據(jù)彈出,存入局部變量表 slot_1

ldc // int 32768

  • 從常量池加載數(shù)據(jù),到操作數(shù)棧
  • 注意:Short.MAX_VALUE = 32767, 這里的32768在編譯期計(jì)算好了

istore_2

  • 將操作數(shù)棧頂數(shù)據(jù)彈出,存入局部變量表 slot_2

iload_1

  • 將局部變量表slot_1中的數(shù)據(jù),讀取到操作數(shù)棧上

iload_2

  • 將局部變量表slot_2中的數(shù)據(jù),讀取到操作數(shù)棧上

iadd

  • 執(zhí)行 add 操作并把結(jié)果放到操作數(shù)棧頂

istore_3

  • 將操作數(shù)棧頂數(shù)據(jù)彈出,存入局部變量表 slot_3

getstatic // Field java/lang/System.out:Ljava/io/PrintStream;

  • 通過常量池先找到中的System.out 對(duì)象
  • getstatic 把System.out 對(duì)象的引用值放入操作數(shù)棧

iload_3

  • 將局部變量表slot_3中的數(shù)據(jù),讀取到操作數(shù)棧上

invokevirtual // Method java/io/PrintStream.println:(I)V

  • 在常量池中定位方法 java/io/PrintStream.println:(I)V
  • 生成新的棧幀
  • 傳遞參數(shù),執(zhí)行新棧幀中的字節(jié)碼
  • 執(zhí)行完畢,彈出棧幀
  • 清除main 操作數(shù)棧內(nèi)容

return

  • 完成main 方法調(diào)用,彈出main 棧幀
  • 程序結(jié)束

2.3 構(gòu)造方法

2.3.1 <cinit>()V

public class Test {

    static int i = 10;

    static {
        i = 20;
    }

    static {
        i = 30;
    }
}

編譯器會(huì)按照從上至下的順序,收集所有static 靜態(tài)代碼塊和靜態(tài)成員變量的代碼,合并成一個(gè)特殊的方法<cinit>()V

 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return
      LineNumberTable:
        line 5: 0
        line 8: 5
        line 12: 10
        line 13: 15

<cinit>()V 方法會(huì)在類加載的初始化階段被調(diào)用。

2.3.2 <init>()V

編譯器會(huì)按照從上到下的順序,收集所有 {} 代碼塊 和 成員變量賦值的代碼塊,形成新的構(gòu)造方法,原始構(gòu)造方法內(nèi)的代碼總是在最后

2.4 方法調(diào)用

public class Test {

    private void test01() {}

    private final void test02() {}

    public void test03() {}

    public static void test04() {}

    public static void main(String[] args) {
        Test test = new Test();
        test.test01();
        test.test02();
        test.test03();
        test.test04();
        Test.test04();
    }
}

{
  public com.lily.jvm.Test();
    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   Lcom/lily/jvm/Test;

  public void test03();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/lily/jvm/Test;

  public static void test04();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 11: 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/lily/jvm/Test 
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #4                  // Method test01:()V
        12: aload_1
        13: invokespecial #5                  // Method test02:()V
        16: aload_1
        17: invokevirtual #6                  // Method test03:()V
        20: aload_1
        21: pop
        22: invokestatic  #7                  // Method test04:()V
        25: invokestatic  #7                  // Method test04:()V
        28: return
}

invokespecial 和 invokestatic 屬于靜態(tài)綁定。在字節(jié)碼指令生成的時(shí)候就知道要調(diào)用哪個(gè)方法

invokevirtual 屬于動(dòng)態(tài)綁定。public 方法在編譯期間不能確定調(diào)用哪個(gè)方法,可能有方法重寫。在運(yùn)行期間確定方法的入口地址。

一個(gè)對(duì)象的初始化

0: new           #2                  // class com/lily/jvm/Test  
3: dup
4: invokespecial #3                  // Method "<init>":()V
7: astore_1

new

  • 使用new關(guān)鍵字在堆中分配內(nèi)存, 分配成功后將該對(duì)象的引用放入操作數(shù)棧

dup

  • 將棧頂?shù)牡刂吩趶?fù)制一份。此時(shí)操作數(shù)棧中有兩個(gè)對(duì)象引用

invokespecial

  • 根據(jù)棧頂?shù)膶?duì)象引用地址調(diào)用相應(yīng)的構(gòu)造方法,調(diào)用完畢后,棧頂只有一個(gè)對(duì)象引用了

astore_1

  • 將操作數(shù)棧中剩余的對(duì)象引用存入本地變量表 slot_1

2.5 多態(tài)的原理

當(dāng)執(zhí)行 invokevirtual指令時(shí)

1.先通過棧幀中的對(duì)象引用找到對(duì)象

2.分析對(duì)象頭,找到對(duì)象的實(shí)際class

3.Class結(jié)構(gòu)中有vtable, 它在類加載的連接階段就已經(jīng)根據(jù)方法的重寫規(guī)則生成好了

4.查表得到方法的具體地址

5.執(zhí)行方法的字節(jié)碼

3. 語法糖-編譯期處理

所謂語法糖,其實(shí)就是指java編譯器把 *.java 源碼編譯為 *.class 字節(jié)碼過程中,自動(dòng)生成和轉(zhuǎn)化的一些代碼,主要是為了減輕程序員負(fù)擔(dān),算是Java編譯器給我們的一個(gè)額外福利(糖)

注意,以下代碼的分析,借助了javap工具,idea的反編譯功能,idea 插件 jclasslib 工具等。編譯器轉(zhuǎn)換的結(jié)果直接就是 *.class文件。以下給出偽代碼

3.1 默認(rèn)構(gòu)造器

public class Candy1{
    
}

編譯成 .class后的代碼

public class Candy1{
    //這個(gè)默認(rèn)構(gòu)造器是編譯器自動(dòng)幫我們加上的
    public Candy1() {
        //調(diào)用父類Object 的無參構(gòu)造方法 java/lang/Object."<init>":()V
        super();
    }
}

3.2 自動(dòng)拆裝箱

這個(gè)特性是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();
    }
}

這些基本類型和包裝類型的轉(zhuǎn)換在jdk 5之后,由編譯器在編譯階段完成

3.3 泛型集合取值

泛型也是在jdk 5開始加入的特性,但java在編譯泛型代碼后,會(huì)執(zhí)行 泛型擦除的動(dòng)作。即泛型信息在編譯為字節(jié)碼之后就丟失了。實(shí)際的類型都當(dāng)做了 Object 類型來做處理

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

所以在取值時(shí),編譯器真正生成的字節(jié)碼中,還要額外做一個(gè)類型轉(zhuǎn)換的操作

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

擦除的是字節(jié)碼的泛型信息,可以看到LocalVariableTypeTable仍然保留方法的參數(shù)的泛型信息

使用反射仍然能獲得泛型信息

3.4 可變參數(shù)

可變參數(shù)也是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");
    }
}

可變參數(shù) String... args 其實(shí)是一個(gè)String[] args.

Java編譯器會(huì)在編譯期間將上述代碼變?yōu)椋?/p>

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"});
    }
}

注意,如果調(diào)用了 foo(), 不會(huì)把 null 傳進(jìn)去,而是傳入一個(gè)空的數(shù)組 foo(new String[]{});

3.5 foreach 循環(huán)

jdk 5開始加入的新特性

public class Candy5_1{
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5}; //數(shù)組賦初值的簡化寫法也會(huì)語法糖
        for(int e : array){
            System.out.Println(e);
        }
    }
}

會(huì)被編譯器轉(zhuǎn)換為:

public class Candy5_1{
    
    public Candy5_1(){}
    
    public static void main(String[] args) {
        int[] array = new int[]{1,2,3,4,5}; //數(shù)組賦初值的簡化寫法也會(huì)語法糖
        for(int i = 0; i < array.length; ++i){
            int e = array[i];
            System.out.Println(e);
        }
    }
}

而集合的循環(huán)

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

實(shí)際被編譯器轉(zhuǎn)換為:

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.interarot();
        while(iter.hasNext()){
            Integer e = (Integer)iter.next();
            System.out.Println(e);
        }
    }
}

3.6 switch 字符串

jdk 7開始 switch 可以作用于字符串和枚舉類,這個(gè)功能其實(shí)也是語法糖。

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 和枚舉使用時(shí),變量不能為null,

會(huì)被編譯器轉(zhuǎn)換為:

public class Candy6_1{
    public Candy6_1() {
        
    }
    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            case 99162322:
                if (str.equals("hello")) {
                    x = 0;
                }
            
            case 113318802:
                if (str.equals("world")){
                    x = 1;
                }
        }
        
        switch (x) {
            case 0 : 
                 System.out.Println("h");
                 break;
            case 1 :
                 System.out.Println("w");
                 break;
        }
    }
}

可以看到執(zhí)行了兩遍 switch , 為什么用兩遍?

hashCode 是為了提高效率,減少可能的比較。而equals 是為了防止hash 沖突。

3.7 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;
        }
    }
}

編譯器轉(zhuǎn)換后代碼

public class Candy7{
    
    //定義一個(gè)合成類,僅jvm可見
    //用來映射枚舉的 ordinal 與數(shù)組元素的關(guān)系
    //枚舉的 ordinal 表示枚舉對(duì)象的序號(hào),從 0 開始
    //即 MALE 的 ordinal()=0, FEMALE 的 ordinal()=1
    static class $MAP{
        //數(shù)組大小即為枚舉元素個(gè)數(shù),這里存儲(chǔ) case 用來比對(duì)的數(shù)字
        static int[] map = new int[2];
        static{
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 1;
        }
    }
    
    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;
        }
    }
}

3.8 枚舉類

jdk 7新增了枚舉類

enum Sex{
    MALE, FEMALE
}

轉(zhuǎn)換后代碼:

//枚舉類不能被繼承
public final class Sex extends Enum<Sex>{
    
    public static final Sex MALE;
    public static final Sex FEMALE;
    public 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);
    }
}

3.9 try-with-resource

jdk 7 新增了對(duì)需要關(guān)閉的資源處理

try(資源變量 = 創(chuàng)建資源對(duì)象) {
    
} catch() {
    
}

其中資源對(duì)象需要實(shí)現(xiàn) AutoCloseable 接口,例如 FileInputStream、FileOutputStream、Connection等接口都實(shí)現(xiàn)了 AutoCloseable .

使用 try-with-resource 可以不用寫 finally 語句塊,編譯器會(huì)幫忙生成資源關(guān)閉代碼

public class Candy9{
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("c:\\ll.txt")) {
            System.out.Println(is);
        } catch(IOException e) {
            
        }
    }
}

編譯器轉(zhuǎn)換為:

public class Candy9{
    
    public Candy9() {
        
    }
    
    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("c:\\ll.txt");
            Throwable t = null;
            
            try {
                System.out.Println(is);
            } catch (Throwable e1) {
                
                //t 是代碼出現(xiàn)異常
                t = e1;
                throw e1;
            } finally {
                //判斷了資源不為空
                if (is != null) {
                    //如果代碼有異常
                    if (t != null) {
                        try{
                            is.close();
                        } catch (Throwable e2) {
                            //如果close出現(xiàn)異常,作為被壓制異常添加
                            //如此設(shè)計(jì)防止異常信息丟失
                            t.addSuppressed(e2);
                        }
                    } else {
                        //代碼沒有異常
                        is.close();
                    }
                }
            }
           
            
        } catch(IOException e) {
            
        }
    }
}

3.10 方法重寫時(shí)的橋接方法

方法重寫時(shí)對(duì)返回值分兩種情況

  • 父子類的返回值完全一致
  • 子類的返回值可以是父類返回值的子類。
class A {
    public Number m() {
        return 1;
    }
}

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

對(duì)于子類編譯器轉(zhuǎn)換為

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

其中橋接方法比較特殊,僅jvm可見,并且與原來的 public Integer m()沒有命名沖突

3.11 匿名內(nèi)部類

public class Candy11{
    public static void test(final int x) {
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                System.out.Println("ok" + x);
            }
        }
    }
}

轉(zhuǎn)換后代碼

//額外生成類
public class Candy11$1 implements Runnable{
    int val$x;
    
    public 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);
    }
}

為什么匿名內(nèi)部類引用局部變量時(shí),局部變量必須是 final?

因?yàn)樵趧?chuàng)建Candy11$1對(duì)象時(shí),將x值賦值給 Candy11$1 對(duì)象的 val$x 屬性,所以 x 不應(yīng)該再發(fā)生變化。如果變化了,那么val$x的值沒有機(jī)會(huì)跟著一起變化

4. 類加載階段

4.1 加載

將類的字節(jié)碼載入方法區(qū)中,內(nèi)部采用 c++ 的數(shù)據(jù)結(jié)構(gòu) instanceKlass 描述類,它的重要 field 有:

  • _java_mirror 即java 類鏡像,例如 String.class,作用是把Klass暴露給Java使用
  • _super 父類
  • _fields 成員變量
  • _methods 方法
  • _constants 常量池
  • _class_loader 類加載器
  • _vtable 虛方法表
  • _itable 接口方法表

如果這個(gè)類還有父類沒有加載,先加載父類

加載和鏈接可能是交替運(yùn)行的
類的加載是懶惰的

注意:instanceKlass 這樣的元數(shù)據(jù)是存儲(chǔ)的方法區(qū)(元空間),_java_mirror存儲(chǔ)在堆中。

類加載.png

4.2 鏈接

1.驗(yàn)證:類是否復(fù)合 jvm 規(guī)范,進(jìn)行安全性檢查

2.準(zhǔn)備:為static 變量分配內(nèi)存空間,設(shè)置默認(rèn)值

  • static 變量在 jdk 7之前存儲(chǔ)在 instanceKlass 末層,從jdk 7 開始存儲(chǔ)于 _java_mirror 末尾(堆中類對(duì)象)。
  • static 變量分配空間和賦值是兩個(gè)步驟,分配空間在準(zhǔn)備階段完成,賦值在初始化階段完成。
  • 如果 static 變量是 final 的基本類型(包括String類型),那么編譯階段值就確定了,賦值在準(zhǔn)備階段完成。
  • 如果 static 變量不是 final 的,但屬于引用類型,那么賦值也會(huì)在初始化階段完成。

3.解析:將常量池中的符號(hào)引用解析為直接引用

public class ClassParseTest {

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassParseTest.class.getClassLoader();
        //loadClass() 方法不會(huì)導(dǎo)致類的解析和初始化
        classLoader.loadClass("com.lily.jvm.C");
    }
}

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

class D {

}

4.3 初始化

初始化其實(shí)就是執(zhí)行類的構(gòu)造方法

4.3.1 <cinit>()V方法

初始化即調(diào)用<cinit>()V方法, 虛擬機(jī)會(huì)保證這個(gè)類的構(gòu)造方法線程安全

4.3.2 發(fā)生的時(shí)機(jī)

類初始化是懶惰的

類初始化的時(shí)機(jī)

  • main方法所在的類,總是會(huì)被首先初始化
  • 首次訪問這個(gè)類的靜態(tài)變量或靜態(tài)方法時(shí)會(huì)引發(fā)類的初始化
  • 子類初始化,如果父類還沒有初始化,會(huì)引發(fā)
  • 子類訪問父類的靜態(tài)變量,只會(huì)觸發(fā)父類的初始化
  • Class.forName 默認(rèn)情況會(huì)觸發(fā)類的初始化
  • new 會(huì)導(dǎo)致初始化

不會(huì)導(dǎo)致初始化的情況

  • 訪問類的 final static 靜態(tài)常量(基本類型和字符串),不會(huì)觸發(fā)初始化。在類的鏈接階段完成
  • 類對(duì)象 .class 不會(huì)觸發(fā)初始化(類加載時(shí)生成_java_mirror對(duì)象)
  • 創(chuàng)建該類的數(shù)組時(shí),不會(huì)觸發(fā)初始化
  • 調(diào)用類加載器的 loadClass() 方法,不會(huì)觸發(fā)類的初始化
  • Class.forName() 第二個(gè)參數(shù)為false時(shí),不會(huì)觸發(fā)類的初始化

5. 類加載器

以jdk 8為例

名稱 加載哪的類 說明
Bootstrap ClassLoader JAVA_HOME/jre/lib 無法直接訪問
Extension ClassLoader JAVA_HOME/jre/lib/ext 上級(jí)為 Bootstrap,顯示為 null
Application ClassLoader classpath 上級(jí)為 Extension
自定義類加載 自定義 上級(jí)為 Application
public class F {

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

public class Load1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.lily.jvm.F");
        System.out.println(aClass.getClassLoader());
    }
}

輸出

bootstrap F init
sun.misc.Launcher$AppClassLoader@18b4aac2

5.1 雙親委派模式

雙親委派模式指,調(diào)用類加載器的 loadClass方法時(shí),查找類的規(guī)則。

注意:這里的雙親并沒繼承關(guān)系

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        //1.檢查本類加載器 是否已經(jīng)加載該類
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //2.有上級(jí)委派上級(jí)loadClass
                    c = parent.loadClass(name, false);
                } else {
                    //3.沒有上級(jí)了(ExtClassLoader),則委派Bootstrap ClassLoader
                    c = findBootstrapClassOrNull(name);
                }
                
            } catch (ClassNotFoundException e) {
                
            }
            
            if (c == null) {
                long t1 = System.nanoTime();
                //4.每一層找不到,調(diào)用findClass方法(每個(gè)類加載器自己擴(kuò)展),來加載
                c = findClass(name);
                
                //5. 記錄耗時(shí)
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

5.2 自定義類加載器

什么時(shí)候需要自定義類加載器?

想加載非 classpath ,隨意路徑下的類文件

都是通過接口來使用實(shí)現(xiàn),希望解耦時(shí),常用在框架設(shè)計(jì)

這些類希望予以隔離,不同應(yīng)用的同名類都可以加載,不沖突,常見tomcat

步驟

1.繼承ClassLoader父類

2.遵從雙親委派機(jī)制,重寫findClass() 方法

3.讀取類文件的字節(jié)碼

4.調(diào)用父類的 defineClass 方法來加載類

5.使用者調(diào)用該類加載器的 loadClass方法

6. 運(yùn)行期優(yōu)化

6.1 即時(shí)編譯

6.1.1 分層編譯

public class JITTest {

    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.println(end - start);
        }
    }
}

運(yùn)行結(jié)果發(fā)現(xiàn)剛開始占用時(shí)間很多,后面突然時(shí)間變短了

原因是什么呢?

JVM將字節(jié)碼執(zhí)行分為 5 個(gè)層次:

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

profiling 是指在運(yùn)行過程中,收集一些程序執(zhí)行狀態(tài)的數(shù)據(jù),例如【方法的調(diào)用次數(shù)】,【循環(huán)的回邊次數(shù)】等

即時(shí)編譯器JIT 和 解釋器 的區(qū)別

  • 解釋器是將字節(jié)碼解釋為機(jī)器碼,下次即使遇到相同的字節(jié)碼,仍會(huì)執(zhí)行重復(fù)的解釋
  • JIT 是將一些反復(fù)執(zhí)行的字節(jié)碼編譯為機(jī)器碼,存入 code cache, 下次遇到相同的代碼,直接執(zhí)行,無需再編譯
  • 解釋器是將字節(jié)碼解釋為針對(duì)所有平臺(tái)通用的機(jī)器碼
  • JIT會(huì)根據(jù)平臺(tái)類型,生成平臺(tái)特定的機(jī)器碼

對(duì)于占據(jù)大部分的不常用代碼,我們無需耗費(fèi)時(shí)間將其編譯成機(jī)器碼,而是采取解釋執(zhí)行。
另一方面,對(duì)于僅占據(jù)小部分的熱點(diǎn)代碼,我們則可以將其編譯成機(jī)器碼,以達(dá)到理想的運(yùn)行速度。
執(zhí)行效率上簡單比較一下 Interpreter < C1 < C2
JIT總的目標(biāo)是發(fā)現(xiàn)熱點(diǎn)代碼,并優(yōu)化。

6.1.2 方法內(nèi)聯(lián)

public class JITTest {

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

    private static int square(final int i) {
        return i * i;
    }
}

如果發(fā)現(xiàn) square 是熱點(diǎn)方法,并且長度不太長時(shí),會(huì)進(jìn)行內(nèi)聯(lián)。
內(nèi)聯(lián):就是把方法內(nèi)代碼拷貝、粘貼到調(diào)用者的位置。

System.out.println(9 * 9);
//還可以進(jìn)行常量折疊優(yōu)化
System.out.println(81);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容