美文网首页jvm
图解jvm--(三)类加载与字节码技术

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

作者: 韩who | 来源:发表于2020-02-06 22:57 被阅读0次

    类加载与字节码技术

    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."<init>":()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               <init>
       #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          // "<init>":()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."<init>":()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."<init>":()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               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               LocalVariableTable
      #13 = Utf8               this
      #14 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_1;
      #15 = Utf8               main
      #16 = Utf8               ([Ljava/lang/String;)V
      #17 = Utf8               args
      #18 = Utf8               [Ljava/lang/String;
      #19 = Utf8               a
      #20 = Utf8               I
      #21 = Utf8               b
      #22 = Utf8               c
      #23 = Utf8               SourceFile
      #24 = Utf8               Demo3_1.java
      #25 = NameAndType        #8:#9          // "<init>":()V
      #26 = Utf8               java/lang/Short
      #27 = Class              #33            // java/lang/System
      #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
      #29 = Class              #36            // java/io/PrintStream
      #30 = NameAndType        #37:#38        // println:(I)V
      #31 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_1
      #32 = Utf8               java/lang/Object
      #33 = Utf8               java/lang/System
      #34 = Utf8               out
      #35 = Utf8               Ljava/io/PrintStream;
      #36 = Utf8               java/io/PrintStream
      #37 = Utf8               println
      #38 = Utf8               (I)V
    {
      public cn.itcast.jvm.t3.bytecode.Demo3_1();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 6: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_1;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=4, args_size=1
             0: bipush        10
             2: istore_1
             3: ldc           #3                  // int 32768
             5: istore_2
             6: iload_1
             7: iload_2
             8: iadd
             9: istore_3
            10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
            13: iload_3
            14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
            17: return
          LineNumberTable:
            line 8: 0
            line 9: 3
            line 10: 6
            line 11: 10
            line 12: 17
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      18     0  args   [Ljava/lang/String;
                3      15     1     a   I
                6      12     2     b   I
               10       8     3     c   I
    }
    SourceFile: "Demo3_1.java"
    
    

    (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."<init>":()V
       #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
       #4 = Class              #27            // cn/itcast/jvm/t3/bytecode/Demo3_2
       #5 = Class              #28            // java/lang/Object
       #6 = Utf8               <init>
       #7 = Utf8               ()V
       #8 = Utf8               Code
       #9 = Utf8               LineNumberTable
      #10 = Utf8               LocalVariableTable
      #11 = Utf8               this
      #12 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_2;
      #13 = Utf8               main
      #14 = Utf8               ([Ljava/lang/String;)V
      #15 = Utf8               args
      #16 = Utf8               [Ljava/lang/String;
      #17 = Utf8               a
      #18 = Utf8               I
      #19 = Utf8               b
      #20 = Utf8               SourceFile
      #21 = Utf8               Demo3_2.java
      #22 = NameAndType        #6:#7          // "<init>":()V
      #23 = Class              #29            // java/lang/System
      #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
      #25 = Class              #32            // java/io/PrintStream
      #26 = NameAndType        #33:#34        // println:(I)V
      #27 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_2
      #28 = Utf8               java/lang/Object
      #29 = Utf8               java/lang/System
      #30 = Utf8               out
      #31 = Utf8               Ljava/io/PrintStream;
      #32 = Utf8               java/io/PrintStream
      #33 = Utf8               println
      #34 = Utf8               (I)V
    {
      public cn.itcast.jvm.t3.bytecode.Demo3_2();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 6: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_2;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1
             0: bipush        10
             2: istore_1
             3: iload_1
             4: iinc          1, 1
             7: iinc          1, 1
            10: iload_1
            11: iadd
            12: iload_1
            13: iinc          1, -1
            16: iadd
            17: istore_2
            18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            21: iload_1
            22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
            25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            28: iload_2
            29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
            32: return
          LineNumberTable:
            line 8: 0
            line 9: 3
            line 10: 18
            line 11: 25
            line 12: 32
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      33     0  args   [Ljava/lang/String;
                3      30     1     a   I
               18      15     2     b   I
    }
    SourceFile: "Demo3_2.java"
    
    
    

    分析:

    • 注意 iinc 指令是直接在局部变量 slot 上进行运算

    • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

    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 "<init>":()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 "<init>":()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." <init>":()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<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();
    

    擦除的是字节码上的泛型信息,可以看到 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."<init>":()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<Ljava/lang/Integer;>;
        Exceptions:
          throws java.lang.Exception
    }
    
    

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

    public Set<Integer> test(List<String> list, Map<Integer, Object> 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<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); 
            } 
        } 
    }
    

    注意 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<Sex> { 
        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."<init>":()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."<init>":()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 初始化

    <cinit> ()V 方法

    初始化即调用 <cinit>()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:<new bootclasspath>

    • 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<DriverInfo> 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<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
     
            // 1)使用 ServiceLoader 机制加载驱动,即 SPI
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
    
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> 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 <S> ServiceLoader<S> load(Class<S> 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--(三)类加载与字节码技术

        本文链接:https://www.haomeiwen.com/subject/ijjpxhtx.html