图解jvm--(三)类加载与字节码技术

类加载与字节码技术

1.类文件结构

根据 JVM 规范,类文件结构如下

ClassFile { 
    u4 magic; //魔数
    u2 minor_version; //小版本号
    u2 major_version; //java 主版本号
    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]; 
}

2.字节码指令

指令 作用
iconst_1 int型常量值1进栈
bipush 将一个byte型常量值推送至栈顶
iload_1 第二个int型局部变量进操作数栈,从0开始计数
istore_1 将操作数栈栈顶 int 型数值存入第二个局部变量,从0开始计数
iadd 栈顶两int型数值相加,并且结果进栈
return 当前方法返回void
getstatic 获取指定类的静态域,并将其值压入栈顶
putstatic 为指定的类的静态域赋值
invokevirtual 调用实例方法
invokespecial 调用超类构造方法、实例初始化方法、私有方法
invokestatic 调用静态方法
invokeinterface 调用接口方法
new 创建一个对象,并且其引用进栈
newarray 创建一个基本类型数组,并且其引用进栈
iinc 1,1 局部变量表的值自加
iinc 1,-1 局部变量表的值自减

2.1 javap 工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件 javap -v

javap -v Demo1_22.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class
  Last modified 2020-1-30; size 534 bytes
  MD5 checksum 5c4213b2f1defff2bb24bf7cbd5ff183
  Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
      //常量池
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."":()V
   #2 = String             #25            // a
   #3 = String             #26            // b
   #4 = String             #27            // ab
   #5 = Class              #28            // cn/itcast/jvm/t1/stringtable/Demo1_22
   #6 = Class              #29            // java/lang/Object
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t1/stringtable/Demo1_22;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               s1
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               s2
  #21 = Utf8               s3
  #22 = Utf8               SourceFile
  #23 = Utf8               Demo1_22.java
  #24 = NameAndType        #7:#8          // "":()V
  #25 = Utf8               a
  #26 = Utf8               b
  #27 = Utf8               ab
  #28 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22
  #29 = Utf8               java/lang/Object
{
 //----------------------------------------构造方法
  public cn.itcast.jvm.t1.stringtable.Demo1_22();
    descriptor: ()V
    flags: ACC_PUBLIC  
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
     //源代码的第几行对应字节码的第几行           
      LineNumberTable:
        line 4: 0
            //本地变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t1/stringtable/Demo1_22;
//-------------------------------
    //-------------------------------main方法
  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
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
       //源代码的第几行对应字节码的第几行           
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 26: 9
      //局部变量表            
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;
}
SourceFile: "Demo1_22.java"

2.2 图解方法执行流程

(1)原始java代码

/*** 演示 字节码指令 和 操作数栈、常量池的关系 */ 
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); 
     }
 }

(2)编译后的字节码文件

D:\IDEAworkplace\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode> javap -v Demo3_1.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2020-1-28; 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."":()V
   #2 = Class              #26            // java/lang/Short
   a/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/itcast/jvm/t3/bytecode/Demo3_1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               
   #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          // "":()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."":()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"

(3)常量池载入运行时常量池

image

(4)方法字节码载入方法区

image

(5)main线程开始运行,分配栈帧内存

(stack=2(栈的深度),locals=4(局部变量表))

image

(6)执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈(操作数栈默认都是4字节)(不够4字节,其长度会补齐 4 个字节),类似的指令还有

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)

  • ldc 将一个 int 压入操作数栈

  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)

  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

image

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1
image
image

ldc #3

  • 从常量池加载 #3 数据到操作数栈

  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

image

istore_2

image
image

iload_1

image

iload_2

image

iadd

image
image

istore_3

image
image

getstatic #4

image
image

iload_3

image
image

invokevirtual #5

  • 找到常量池 #5 项

  • 定位到方法区 java/io/PrintStream.println:(I)V 方法

  • 生成新的栈帧(分配 locals、stack等)

  • 传递参数,执行新栈帧中的字节码

image
  • 执行完毕,弹出栈帧

  • 清除 main 操作数栈内容

image

return

  • 完成 main 方法调用,弹出 main 栈帧

  • 程序结束

2.3 练习-分析 a++

目的:从字节码角度分析 a++ 相关题目

源码:

/**
 * 从字节码角度分析 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);
    }
}

字节码:

D:\IDEAworkplace\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode> javap -v Demo3_2.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_2.class
  Last modified 2020-1-28; 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."":()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               
   #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          // "":()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."":()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

image
image

a++

image
image

++a

image
image

add

image

a--

image
image

add

image
image

2.4 练习 - 判断结果

从字节码角度分析,先 istore 0 ,进操作数栈,然后,再将x 的局部变量表执行 iinc 加1,然后,把操作数栈的数 istore 回去 x 所在的局部变量表,所以,x一直为0

public class Demo3_6_1 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x);
    }
}

//结果 为 0

2.5 方法调用

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();  //invokespecial
        d.test2();  //invokespecial
        d.test3();  //invokevirtual
        d.test4(); //invokestatic
        Demo3_9.test4(); //invokestatic
        d.toString();
    }

}

 stack=2, locals=2, args_size=1
         0: new           #3                  // class cn/itcast/jvm/t3/bytecode/Demo3_9
         3: dup
         4: invokespecial #4                  // Method "":()V
         7: astore_1
         8: aload_1
         9: invokespecial #5                  // Method test1:()V
        12: aload_1
        13: invokespecial #6                  // Method test2:()V
        16: aload_1
        17: invokevirtual #7                  // Method test3:()V
        20: aload_1
        21: pop             //说明通过对象调用静态方法,会弹出栈,直接调用
        22: invokestatic  #8                  // Method test4:()V
        25: invokestatic  #8                  // Method test4:()V
        28: aload_1
        29: invokevirtual #9                  // Method toString:()Ljava/lang/String;
        32: pop
        33: return

通过对象调用静态方法,实际上是load 后,又出栈了,说明通过对象调用静态方法,会弹出栈,直接调用。

静态方法是不需要对象的,直接使用

2.6 new 关键字原理

         0: new           #3                  // class cn/itcast/jvm/t3/bytecode/Demo3_9
         3: dup
         4: invokespecial #4                  // Method "":()V
         7: astore_1

new 有两步,第一步是先分配一个对象在堆空间需要的内存,分配成功,会把对象的引用放入操作数栈,然后 执行 dup 复制一份对象引用? 为什么? 复制一份对象,来执行init方法,构造方法,执行完后,这个对象就出栈了,所以需要复制一份,然后会执行 store 把此时栈顶的对象引用存储到局部变量表中的(即赋值给 new 出这个对象的引用 )

image

2.7 多态原理

在类的链接阶段,就确定了虚方法表(vtable) ,确定下一个类加载时执行方法的先后顺序

当执行一个对象的 invokevirtual指令时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际class
  3. class结构中有 vtable ,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

2.8 异常处理

try - catch

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
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2         //把异常对象的局部变量存到e中
         9: bipush        20
        11: istore_1
        12: return
                  
      //异常表,检测第2行到第5行范围内的异常,进行异常类型匹配,一旦有异常,则会到第执行第8行            
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception
   
      //局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/Exception;
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 8
          locals = [ class "[Ljava/lang/String;", int ]
          stack = [ class java/lang/Exception ]
        frame_type = 3 /* same */
}

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围

​ 内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号

  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

多个 try-catch块

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
      LineNumberTable:
       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

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

multi-catch的情况

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;
      StackMapTable: number_of_entries = 2

finally

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
         0: iconst_0
         1: istore_1           // 0 -> i
         2: bipush        10      //try---
         4: istore_1              // 10 -> i
         5: bipush        30      //finally
         7: istore_1             //30 -> i
         8: goto          27     //return 
        11: astore_2             //catch Exception -> e 
        12: bipush        20
        14: istore_1
        15: bipush        30        //finally
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30       //finally
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      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
      StackMapTable: number_of_entries = 3

}

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流

2.9 finally练习

题目一:

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {

        try {
            return 10;
        } finally {
            return  20;
        }
    }
}

//结果是 20 
  public static int test();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=0
         0: bipush        10  // <- 10放入栈顶
         2: istore_0          // 10 -> slot 0 (从栈顶移除了)
         3: bipush        20   // <- 20 放入栈顶 
         5: ireturn            // 返回栈顶 int(20)
         6: astore_1           // catch any --> slot 1
         7: bipush        20     // <- 20 放入栈顶
         9: ireturn              //返回栈顶 int(20)
      Exception table:
         from    to  target type
             0     3     6   any
}

  • 由于 fifinally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准

  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子

  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,最好不要在finally 里面 return

例如:下面的代码,不会出现除零异常

    public static int test() {

        try {
            return 10/0;
        } finally {
            return  20;
        }
    }
}

题目二:

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {

        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

//结果是 10
 public static int test();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0
         0: bipush        10       // <- 10 放入栈顶
         2: istore_0               // 10 -> i
         3: iload_0                // <- i(10)
         4: istore_1               // 10 -> slot 1 ,暂存至 slot 1, 目的时为了固定返回值
         5: bipush        20       // <- 20 放入栈顶
         7: istore_0               // 20 ->i
         8: iload_1                // <- slot 1(10) 载入 slot 1 暂存的值
         9: ireturn                //返回栈顶的 int(10)
        10: astore_2
        11: bipush        20
        13: istore_0
        14: aload_2
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I


2.10 synchronized

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                  // 执行构造方法
         7: astore_1                         //lock引用 -> lock     
         8: aload_1                        //对象加载到操作数栈 <-lock (synchronized开始)
         9: dup 
        10: astore_2                      //lock 引用 -> slot2 
        11: monitorenter                   // monitorenter (lock引用) 加锁操作
        12: getstatic     #3                  //System.out
        15: ldc           #4                  // String ok
        17: invokevirtual #5                   // System.out
        20: aload_2                          // <- slot2  (lock引用)
        21: monitorexit                   // monitorexit 解锁
        22: goto          30
                  //--------------------出现异常-----
        25: astore_3                     // any -> slot 3
        26: aload_2                      //<- slot 2(lock 引用)
        27: monitorexit                  // monitorexit 解锁
        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;

}

3.编译器处理

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成

和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃

嘛)

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,

编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并

不是编译器还会转换出中间的 java 源码,切记。

3.1 默认构造器

public class Candy1 { 
 }
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的 
  public Candy1() {
    super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." ":()V 
  } 
}

3.2 自动拆装箱

这个特性是 JDK 5 开始加入的, 代码片段1 :

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

这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :

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

3.3 泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息

在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Candy3 { 
    public static void main(String[] args) { 
        List 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();

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 15: 20
        line 31: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
    //局部变量类型表                                 
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List;
    Exceptions:
      throws java.lang.Exception
}

使用反射,仍然能够获得这些信息:

public Set test(List list, Map map) {
        return null;
    }
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        System.out.println("原始类型 - " + parameterizedType.getRawType());
        Type[] arguments = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < arguments.length; i++) {
            System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
        }
    }

}

输出:
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

3.4 可变参数

可变参数也是 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 进去

3.5 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 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 list = Arrays.asList(1, 2, 3, 4, 5); 
        Iterator iter = list.iterator(); //获取迭代器
        while(iter.hasNext()) { 
            Integer e = (Integer)iter.next(); 
            System.out.println(e); 
        } 
    } 
}

注意 foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其

中 Iterable 用来获取集合的迭代器( Iterator )

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

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

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

3.8 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

enum Sex {
  MALE, FEMALE 
}

转换后代码:

public final class Sex extends Enum { 
    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); 
    } 
}

3.9 try-with-resourses

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`:

try(资源变量 = 创建资源对象){
} 
catch( ) { 
    
}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStreamOutputStream

ConnectionStatementResultSet 等接口都实现了 AutoCloseable ,使用 try-with-

resources 可以不用写 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();
        } 
    }
}

会被转换为:

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

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信

息的丢失(想想 try-with-resources 生成的 fifianlly 中如果抛出了异常):

public class Test6 {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) { 
            int i = 1/0; 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
    } 
}

class MyResource implements AutoCloseable {
    public void close() throws Exception { 
        throw new Exception("close 异常"); 
    } 
}

输出:异常都不会丢

java.lang.ArithmeticException: / by zero 
    at test.Test6.main(Test6.java:7) 
    Suppressed: java.lang.Exception: close 异常
        at test.MyResource.close(Test6.java:18)
        at test.Test6.main(Test6.java:6)

3.10 方法重新时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致

  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)

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() 没有命名冲突,可以

用下面反射代码来验证:

for (Method m : B.class.getDeclaredMethods()) {
    System.out.println(m); 
}

会输出:

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

3.11 匿名内部类

源代码:

public class Candy11 { 
    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();
    } 
}

引用局部变量的匿名内部类,源代码:

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 的:

因为在创建 Candy11 $1 对象时,将 x 的值赋值给了 Candy11 $1 对象的 val

属 性 , 不 应 该 再 发 生 变 化 了 , 如 果 变 化, 那 么 val$x 属性没有机会再发生变化

4.类加载阶段

4.1 加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 Kclass 暴露给 java 使用

  • _super 即父类

  • _fields 即成员变量

  • _methods 即方法

  • _constants 即常量池

  • _class_loader 即类加载器

  • _vtable 虚方法表

  • _itable 接口方法表

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

  • 注意

    instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中

    可以通过前面介绍的 HSDB 工具查看

image

4.2 链接

验证

验证类是否符合 JVM规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数( 修改 ca fe ba by ),在控制台运行

image

准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾(在堆)

  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

public class Load8 {

    static int a;
    static int b = 10;
    static final int c = 20;
    static final String d = "hello";
    static final Object e = new Object();
}

字节码

{
  static int a;
    descriptor: I
    flags: ACC_STATIC

  static int b;
    descriptor: I
    flags: ACC_STATIC   //准备阶段先分配空间,然后在初始化阶段才赋值

  static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20    //final 直接赋值

  static final java.lang.String d;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String hello   //finale 直接负责

  static final java.lang.Object e;
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL   //引用类型,那么赋值也会在初始化阶段完成

  public cn.itcast.jvm.t3.load.Load8();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/load/Load8;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: new           #3                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."":()V
        12: putstatic     #4                  // Field e:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 7: 0
        line 10: 5
}

解析

将常量池中的符号引用解析为直接引用

/**
 * 解析的含义
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {          ClassLoader classloader = Load2.class.getClassLoader();
       //loadClass 方法不会导致类的解析和初始化                                                 
       Class c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
       // new C();
      //只有通过new 方式,才能实现对类的解析
        System.in.read();
    }
}

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

class D {

}

4.3 初始化

()V 方法

初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

导致类初始化的情况

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName

  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

  • 类对象.class 不会触发初始化

  • 创建该类的数组不会触发初始化

  • 类加载器的 loadClass 方法

  • Class.forName 的参数 2 为 false 时

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

4.4 练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);

    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;  // Integer.valueOf(20) 会导致初始化
    static {
        System.out.println("init E");
    }
}


//结果
10
hello
init E
20

典型应用 - 完成懒惰初始化单例模式

public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();    //结果,不会调用初始化
        
        //结果时执行了一次初始化,第二次调用类以及初始化,直接返回
        Singleton.getInstance();
        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;
    }
}

5.类加载器

以 JDK 8 为例:

名称 加载那些类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

5.1 启动类加载器

用Boorstrap 类加载器加载类:

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

执行

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

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null   //由启动类加载器加载的

-Xbootclasspath 表示设置 bootclasspath

其中 /a:. 表示将当前目录追加至 bootclasspath 之后

可以用这个办法替换核心类

  • java -Xbootclasspath:

  • java -Xbootclasspath/a:<追加路径>

  • java -Xbootclasspath/p:<追加路径>

5.2 拓展类加载器

public class G { 
    static { 
        System.out.println("classpath G init");
    } 
}
/**
 * 演示 扩展类加载器
 * 在 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());
    }
}

输出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

写一个同名的类

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

打个 jar 包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class 
已添加清单 
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext

重新执行 Load5_2

输出

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

5.3 双亲委派机制

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

注意

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

ClassLoader类中的 loadClass 方法

   protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //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) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //4.每一层找不到,调用findClass 方法(每个类加载器直接拓展)来加载
                    c = findClass(name);

                    //5.记录耗时
                    // this is the defining class loader; record the stats
                    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.4 线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager {


    
    //注册驱动的集合
    private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();


    private DriverManager(){}


    //初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但

JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在

DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
 
        // 1)使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction() {
            public Void run() {

                ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator driversIterator = loadedDrivers.iterator();

   
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);
         // 2)使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                
                // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
                Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此

可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

image

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); 
Iterator<接口类型> iter = allImpls.iterator(); 
while(iter.hasNext()) { 
    iter.next(); 
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC

  • Servlet 初始化器

  • Spring 容器

  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static  ServiceLoader load(Class service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:

 private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

5.5 自定义类加载器

问问自己,什么时候需要自定义类加载器

  • 1)想加载非 classpath 随意路径中的类文件

  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计

  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类

  2. 要遵从双亲委派机制,重写 findClass 方法

​ 注意不是重写 loadClass 方法,否则不会走双亲委派机制

  1. 读取类文件的字节码

  2. 调用父类的 defineClass 方法来加载类

  3. 使用者调用该类加载器的 loadClass 方法

你可能感兴趣的:(图解jvm--(三)类加载与字节码技术)