4.JVM-类加载器与字节码技术

类加载和字节码技术

1.类文件结构

JVM规范,类文件结构

1.1 魔数

0-3字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

1.2 版本

4-7字节,表示类的版本 00 34(52) 表示是java8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

1.3 常量池

8-9字节,表示常量池长度, 00 23(35) 表示常量池#1-#34项,注意#0项不计入,也没有值。
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09


第#1项0a表示一个Method信息,00 06和00 15(21)表示他引用了常量池中#6和#21项来获得这个方法的【所属类】和【方法名】。
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • 0A(16)=10,查表可知是方法引用信息。

第#2项09表示一个Filed信息,00 16(22)和00 17(23) 表示它引用了常量池中#22和#23来获得这个成员变量的【所属类】和【成员变量名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#3项08表示一个字符串常量名称,00 18(24)表示它引用了常量中中#24项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#4项0a表示一个Method信息00 19(25)和00 1a(26)表示它引用了常量池中#25和#26项来获得这个类的【所属类】和【方法名】
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#5项07表示一个class信息,00 1b(27)表示它引用了常量池中的#27项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

第#6项07表示一个class信息,00 1c(28)表示它引用了常量池中的#28项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

1.4 访问标识和继承信息


字段表结构

2. 字节码指令

2.1 Javap 工具

命令 javap -v .class文件( -v 代表显示详细的信息)

1) 原始Java代码
public class ByteCode {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;//32767 + 1
        int c = a + b;
        System.out.println(c);//32778
    }
}
2) 编译后的字节码文件
Classfile /H:/WorkSpace/AlgorithmSP/out/production/JVMStudy/cn/zyj/bytecode/ByteCode.class
  Last modified 2020-4-22; size 618 bytes
  MD5 checksum e2ef86ed7fa247db38262639a522f8d8
  Compiled from "ByteCode.java"
public class cn.zyj.bytecode.ByteCode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768 /*超过了Short的范围存储在常量池中*/
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/zyj/bytecode/ByteCode
   #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/zyj/bytecode/ByteCode;
  #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               ByteCode.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 /*接收int 类型的变量 返回值为 void*/
  #31 = Utf8               cn/zyj/bytecode/ByteCode
  #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.zyj.bytecode.ByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/zyj/bytecode/ByteCode;

  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 /*stack 操作数栈的最大深度,locals 局部变量表的长度*/
         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 5: 0
        line 6: 3
        line 7: 6
        line 8: 10
        line 9: 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: "ByteCode.java"

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


  运行时常量池是方法区的一个组成部分,这里只是把他单拎出来看。(他只是吧 class 文件常量池中的数据,存入到了运行时常量池中)

  超过Short整数的最大值,就会存储在常量池中,在Short范围内的数字和字节码指令存在一起。

println:(I)V 代表接收int 类型的变量 返回值为 void 的println()方法

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

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

/ stack 操作数栈的最大深度,locals 局部变量表的长度 /
操作数栈的宽度为四个字节

6) 执行引擎工作
bipush 10
  • 将一个 byte压入操作数栈(其长度会补齐4个字节),类似的指令还有
  • sipush将一个short压入操作数栈(其长度会补齐4个字节)
  • ldc将一个int压入操作数栈
  • ldc2_w将一个long压入操作数栈(分两次压入,因为long是8个字节)
  • 这里小的数字都是和字节码指令存在一起,超过short范围的数字存入了常量池

操作数栈的宽度为 4 个字节

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

ldc #3
  • 从常量池加载 #3 数据到操作数栈
  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

istore_2


iload_1 和 iload_2

iadd

istore_3
getstatic #4

iload_3
invokevirtual #5
  • 找到常量池 #5项
  • 定位到方法区 java/io/PrintStream.println: (I)V 方法
  • 生成新的栈帧(分配locals、stack等)
  • 传递参数,执行新栈帧中的字节码

每个方法在执行的同时都会创建一个栈帧(Stack Frame)

  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容
return
  • 完成 main 方法调用,弹出 main 栈帧
  • 程序结束

2.2 练习 i++

public class ByteCode {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}


分析:

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算
  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

2.3 条件判断指令


几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • 而对于long类型、float类型 和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpg、dcmpk fcmpg、fcmpk Icmp),运算指令会返回一个整型值到操作数栈中,随后再执行 int类型的条件分支比较操作来完成整个分支跳转。
  • goto 用来进行跳转到指定行号的字节码
public class ByteCode {
    public static void main(String[] args) {
        int a = 0;
        if(a == 0){
            a = 10;
        }else {
            a = 20;
        }
    }
}

  字节码如下:

stack=1, locals=2, args_size=1
         0: iconst_0 /*比较小的数是用 iconst 表示的(-1 ~ 5)*/
         1: istore_1
         2: iload_1
         3: ifne          12 /*如果成立,即不等于 0 会跳到12号*/
         6: bipush        10/*上条语句不成立,顺序执行该语句*/
         8: istore_1
         9: goto          15/*跳到 15 号*/
        12: bipush        20
        14: istore_1
        15: return

2.4 循环控制指令

while…

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

  字节码如下:

 stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     14
         8: iinc          1, 1
        11: goto          2
        14: return

do…while

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

  字节码如下:

    stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: bipush        10
         8: if_icmplt     2
        11: return

for 循环

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

  字节码如下:

stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     14
         8: iinc          1, 1
        11: goto          2
        14: return

注意
比较 while 和 for 的字节码,发现他们是一模一样的,殊途同归

2.5 练习-判断结果

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

结果:
0

分析:
  可以转换成如下字节码: x = x++; iload_2(x的值存在2号slot)、iinc(slot中的值自增了变为了1,但是操作数栈的值未变还是 0)、istore_2(将操作数栈的数弹出到 2号slot 中 值依旧是0)。

2.6 构造方法

1) < cinit >( )V
public class ByteCode {
    static int i = 10;
    static{
        i = 20;
    }
    static {
        i = 30;
    }
    public static void main(String[] args) {
        System.out.println(i);//30
    }
}

  编译器会按照从上至下的顺序,手机所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 < cinit >( )V
字节码如下:

stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2 /*赋值 static */  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return

  < cinit >( )V 方法会在类加载的初始化阶段被调用

2) < init >( )V
public class ByteCode {
    private String a = "s1";
    {
        b = 20;
    }
    private int b = 10;
    {
        a = "s2";
    }

    public ByteCode(String a,int b){
        this.a = a;
        this.b = b;
    }

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

  编译器会按从上至下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。

public cn.zyj.bytecode.ByteCode(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String s1
         7: putfield      #3 /*将操作数栈的栈顶元素赋值给 #3*/ // Field a:Ljava/lang/String;
        10: aload_0
        11: bipush        20
        13: putfield      #4                  // Field b:I
        16: aload_0
        17: bipush        10
        19: putfield      #4                  // Field b:I
        22: aload_0
        23: ldc           #5                  // String s2
        25: putfield      #3                  // Field a:Ljava/lang/String;
        28: aload_0                           //------------------------------
        29: aload_1                           // <-slot 1(a) "s3"            丨
        30: putfield      #3                  // Field a:Ljava/lang/String;  丨
        33: aload_0                                                          丨
        34: iload_2                           // <-slot 2(b) 30              丨
        35: putfield      #4                  // Field b:I--------------------
        38: return

2.7 方法调用★★★★

public class ByteCode {
   public ByteCode(){}
   private void test1(){}
   private final void test2(){}
   public void test3(){}
   public static void test4(){}
   
   public static void main(String[] args) {
        ByteCode d = new ByteCode();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        ByteCode.test4();
    }
}

字节码如下:

stack=2, locals=2, args_size=1
         0: new           #2/*在堆空间分配内存*/  // class cn/zyj/bytecode/ByteCode
         3: dup  /*复制一份对象的引用放入操作数栈中 操作数栈中共有两份 一份在 4 调用构造方法(调用结束就弹出) 一份在 7 存入slot 1中*/
         4: invokespecial #3/*构造方法方法*/     // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #4/*普通的private方法*/ // Method test1:()V
        12: aload_1
        13: invokespecial #5/*private final方法*/ // Method test2:()V
        16: aload_1
        17: invokevirtual #6/*普通的public方法*/ // Method test3:()V
        20: aload_1
        21: pop
        22: invokestatic  #7/*对象调用static方法*/ // Method test4:()V
        25: invokestatic  #7/*类名调用static方法*/ // Method test4:()V
        28: return

  invokespecial 和 invokestatic 都是静态绑定(他们在字节码指令生成的时候就知道如何找到是那个类的哪个方法)说明私有方法和静态方法都是确定的,而public方法可能出现方法重写(覆写),所以在编译期间并不能确定自己是调用哪个对象的方法(可能是子类的也可能是父类的),invokevisual是动态绑定的需要在运行的时候确定。从性能上来讲 invokespecial 和 invokestatic 性能较高(因为属于静态绑定,将来直接找到代码执行地址)而 invokevisual 较低(需要查询查询多次才能确定方法的入口地址)

由上字节码可知:
  使用对象调用 static 方法过程: 先将对象的引用入栈,然后再弹出,在调用方法
  使用类名调用 static 方法过程:直接调用方法
  由此可见 使用类名直接调用静态方法比使用对象调用静态方法

2.8 多态的原理★★★★

/**
 * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
 * 64位的虚拟机,为了节省内存空间采用了指针压缩的技术
 */
public class ByteCode {

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

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

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

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("啃骨头");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("吃鱼");
    }
}
1)运行代码

  停在 Syste.in.read()方法上,这是运行 jps 获取进程 id

2)运行 HSDB 工具

  能够看到虚拟机中比较底层的内存状态和内存地址

进入 JDK 安装目录,执行

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

  进入图形界面 attach 进程 id

3)查找某个对象

  打开 Tools 中的 Find Object by Query
  select 对象的类型别名 from 对象的类型(全限定类名)

4)查看对象内存结构


这里的 Dog 类比较简单只包括含了头的信息,对象头一共是 16 个字节

  • 前 8 个字节叫 markword 里面包含:对象的hash码,对象将来加锁时的锁标记
  • 后 8 个字节是对象的类型指针
5)查看对象 Class 的内存地址

6)查看类的 vtable

  在 Tools 中打开 Inspector 窗口 将类型地址输入进去

  虚方法 所以在链接的时候就确定了每一个虚方法的入口地址。
  虚方法表在类结构中的最后

  vtable 和 Class 的相对偏移地址是 0X1B8

7)验证方法地址

  打开 Tools 中的 Class Browser


  发现 Animal类中的 toString() 方法和上面查的 Dog 类中的 toString() 方法地址相同

  发现 Object类中的 finalize() 、equals() 、hashCode() 、clone() 方法和上面查的 Dog 类中的 相应方法地址相同

8)小结★★★★

当执行invokevisual指令时,

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

2.9 异常处理★★★★

try-catch
public class ByteCode {
    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 /*将异常对象的引用地址存入局部变量表 slot2*/
         9: bipush        20
        11: istore_1
        12: return
 Exception table:/*异常表*/
         from    to  target type
             2     5     8   Class java/lang/Exception/*检测第 2 行到第 4 行(含头不含尾)*/
             /*如果范围内出现异常,会先进行一个类型匹配,看发生的异常和声明的Exception异常是否一致,如果一直就会进入第八行*/
      LineNumberTable:...
      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位置
多个 single-catch 块的情况
public class ByteCode {
    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;
        }
    }
}
 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 ByteCode {
    public static void main(String[] args) {
        int i = 0;
        try {
            Method test = ByteCode.class.getMethod("test");
            test.invoke(null);
        } catch (NoSuchMethodException|IllegalAccessException| InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    public static void test(){
        System.out.println("ok");
    }
}
 stack=3, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: ldc           #2                  // class cn/zyj/bytecode/ByteCode
         4: ldc           #3                  // String test
         6: iconst_0
         7: anewarray     #4                  // class java/lang/Class
        10: invokevirtual #5                  // Method java/lang/Class.getMetho
d:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        13: astore_2
        14: aload_2
        15: aconst_null
        16: iconst_0
        17: anewarray     #6                  // class java/lang/Object
        20: invokevirtual #7                  // Method java/lang/reflect/Method
.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        23: pop
        24: goto          32
        27: astore_2
        28: aload_2
        29: invokevirtual #11                 // Method java/lang/ReflectiveOper
ationException.printStackTrace:()V
        32: return
      Exception table:
         from    to  target type
             2    24    27   Class java/lang/NoSuchMethodException
             2    24    27   Class java/lang/IllegalAccessException
             2    24    27   Class java/lang/reflect/InvocationTargetException
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           14      10     2  test   Ljava/lang/reflect/Method;
           28       4     2     e   Ljava/lang/ReflectiveOperationException;
            0      33     0  args   [Ljava/lang/String;
            2      31     1     i   I
finally
public class ByteCode {
    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			// 20 -> i                 丨
        15: bipush        30	// finally                 丨
        17: istore_1			// 30 -> i                 丨
        18: goto          27	// return-------------------
        21: astore_3			// catch any -> slot 3 -----
        22: bipush        30	// finally                 丨
        24: istore_1			// 30 -> i                 丨
        25: aload_3				// <- slot 3			   丨
        26: athrow				// throw--------------------
        27: return				
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any //剩余的异常类型,比如Error
            11    15    21   any //剩余的异常类型,比如Throwable
      LineNumberTable:...
      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

  • 可以看到finally中的代码被复制了3份,分别放入try流程,catch流程以及catch剩余的异常类型流程
finally 出现了 return ★★★★
public class ByteCode {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);// 20
    }
    public static int test(){
        try {
            return 10;
        }finally {
            return 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
      LineNumberTable:...
      StackMapTable:...

  • 由于finally中的ireturn被插入了所有可能的流程,因此返回结果肯定以finally的为准
  • 跟上例中的finally相比,发现没有athrow 了,这告诉我们:如果在finally中出现了 return,会吞掉异常,可以试一下下面的代码
public class ByteCode {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }
    public static int test(){
        try {
            int i = 1/0;//不会出现ArithmeticException: / by zero
            return 10;
        }finally {
            return 20;
        }
    }
}
finally 对返回值的影响 ★★★★
public class ByteCode {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result); // 10
    }
    public static int test(){
        int i = 10;
        try {
            return i;
        }finally {
            i = 20;
        }
    }
}
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
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I
      StackMapTable:...

2.10 synchronized

public class ByteCode {
    public static void main(String[] args) {
        Object lock = new Object();//对应字节码0~8
        synchronized (lock){
            System.out.println("ok");// 12~17 
        }
    }
}
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2	// new Object
         3: dup
         4: invokespecial #1	// invokespecial <init>:()V
         7: astore_1			// lock 引用 -> lock
         8: aload_1				// <- lock (synchronized开始)
         9: dup
        10: astore_2			// lock引用 -> slot 2
        11: monitorenter		//monitorenter(lock引用)
        12: getstatic     #3    // <- System.out
        15: ldc           #4    // <- "ok"
        17: invokevirtual #5    // invokevisual println:(Ljava/lang/String;)V
        20: aload_2				// <- slot 2(lock 引用)
        21: monitorexit			// monitorexit(lock引用)
        22: goto          30
        25: astore_3			// any -> slot 3
        26: aload_2				// <- slot 2(lock引用)
        27: monitorexit			// monitorexit(lock引用)
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any/*有异常去 25 */
            25    28    25   any
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 25
          locals = [ class "[Ljava/lang/String;", class java/lang/Object, class
java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

注意
方法级别的 synchronized 不会在字节码指令中有所体现

3. 编译期处理

  所谓的 语法糖 ,其实就是指Java编译器把*. java源码编译为*. class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利(给糖吃)

  注意,以下代码的分析,借助了 javap工具,idea的反编译功能,idea插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的Java源码方式,并不是编译器还会转换出中间的Java源码,切记。

3.1 默认构造器

源码.java

public class Candy {}

字节码.class

 public cn.zyj.bytecode.Candy();
    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

编译成class后的代码:

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

3.2 自动拆装箱

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

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

  显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在JDK5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段被转换为代码片段2

3.3 泛型集合取值

  泛型也是在JDK 5开始加入的特性,但Java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

  所以在取值时,编译器真正生成的字节码中,还要额额外做一个类型转换的操作:

  如果前面的X变量类型修改为int基本类型那么最终生成的字节码是:

  JDK 5 之后这些麻烦的事就不用自己做了。
  泛型擦除,擦除的是字节码上的泛型信息,可以看到  LocalVariableTypeTable仍然保留了方法参数泛型的信息

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


输出

3.4 可变参数

  可变参数也是JDK 5开始加入的新特性:
例如:

  可变参数String… args其实是一个String[] args,从代码中的赋值语句中就可以看出来。 同样Java编译器会在编译期间将上述代码变换为:

注意
  如果调用了 foo( ) 则等价代码为foo(new String[ ]{ }),创建了一个空的数组,而不会传递null进去

3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

会被编译器转换为:

而集合的循环:

实际被编译器转换为迭代器的调用:

注意
  foreach循环写法,能够配合数组,以及所有实现了 Iterable接口的集合类一起使用,其中Iterable用 来获取集合的迭代器(Iterator)

3.6 switch 字符串

  从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

注意
  switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚

会被编译器转换为:

  可以看到,执行了两遍switch,第一遍是根据字符串的hashCode和equals将字符串的转换为相应byte类 型,第二遍才是利用byte执行进行比较。

  hashCode是为了提高效率,减少可能的比较;而equals是为了防止hashCode冲突,例如 BM 和 C. 这两个字符串的hashCode值都是2123。

3.7 switch 枚举

switch 枚举的例子,原始代码:

转换后的代码:

3.8 枚举类

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

转换后的代码:

3.9 try-with-resource

JDK7 开始新增了对需要关闭的资源处理的特殊语法 ‘try-with-resource’:

  其中资源对象需要实现 AutoCloseable 接口,例如 InputStreamx、OutputStream、 Connection、Statement、ResultSet 等接口都实现了 AutoCloseable,使用 try-with-resources 可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:

会被转换为:

  为什么要设计一个addSuppressed(Throwable e)(添加被压制异常)的方法呢?是为了防止异常信息的丢失 (想想 try-with-resource 生成的fianlly中如果抛出了异常):

输出:

  如以上代码所示,两个异常信息都不会丢失。

3.10 方法重写时的桥接方法

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

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

    对于子类,java 编译器会做如下处理:

      其中合成桥接方法比较特殊,仅对 Java 虚拟机可见,并且与原来的public Integer m()没有命名冲突,可以用下面 反射代码来验证:

    会输出:

3.11匿名内部类

源代码:

转换后的代码:

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

转换后代码:

注意
  这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$ 1 对象 时,将x的值赋值给了 Candy11$ 1 对象的 valx 属性,所以 x 不应该再发生变化了,如果变化,那么 valx 属性没有机会再跟着一起变化

4. 类加载阶段★★★

4.1 加载

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

  • _java_mirror即Java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给 Java 使用
  • _super即父类
  • _fields即成员变量
  • _methods即方法
  • _constants即常量池
  • _classloader即类加载器
  • _vtable虚方法表
  • _itable接口方法表

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

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

注意

  • instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中
  • 可以通过HSDB工具查看

4.2 链接

验证

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

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

准备

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

  • static变量在JDK 7之前存储于instanceKlass末尾,从JDK7开始,存储于 _java_mirror 末尾
  • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果static变量是final的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
解析

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

  loadClass 只是加载了类 C 并没有导致 解析和初始化的发生 类D就不会被加载进来。
  使用 HSDB 查看类 C:

  可以看到类 D 是 UnresolvedClass 未被解析的类
  将 loadClass 改为 new C() 再次使用 HSDB 查看 C:

4.3 初始化

<cinit >( )V 方法

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

发生时机

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

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象 .class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName 的参数 2 为 false 时

实验

public class Load {
    static {
        System.out.println("main init");
    }

    //下方都会输出 main init
    public static void main(String[] args) throws ClassNotFoundException {
//        //1. 静态常量(static final)不会触发初始化
//        System.out.println(B.b); // 5.0
//        //2.类对象.class不会触发初始化
//        System.out.println(B.class); // class cn.zyj.bytecode.B
//        //3.创建该类的数组不会触发初始化
//        System.out.println(new B[0]); //[Lcn.zyj.bytecode.B;@1b6d3586
//        //4.不会初始化类 B,但会加载 B、A
//        ClassLoader c1 = Thread.currentThread().getContextClassLoader();
//        c1.loadClass("cn.zyj.bytecode.B"); // 无输出
//        //5.不会初始化类 B,但会加载 B、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.zyj.bytecode.B",false,c2); // 无输出


//        // 1.首次访问这个类的静态变量或静态方法时
//        System.out.println(A.a);// a init; 0
//        // 2.子类初始化,如果父类还没初始化,会引发
//        System.out.println(B.c);// a init; b init; false
//        // 3.子类访问父类静态变量,只触发父类初始化
//        System.out.println(B.a);// a init; 0
//        // 4.会初始化类B, 并先初始化类A
//        Class.forName("cn.zyj.bytecode.B");// a init; b init
    }
}

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 Load2 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);//前两句不会使 E 初始化
        System.out.println(E.c);//这句会使 E 初始化
    }
}
class E {
    public static final int a = 10;
    public static final String b = "Hello";
    public static final  Integer c = 20;//Integer.valueOf(20) 不会在准备阶段完成赋值在初始化阶段完成赋值
}

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

public class Load3{
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
    }
}

class Singleton {
    private Singleton(){}
    // 内部类中保存单例
    static {
        System.out.println("Singleton init");
    }
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();

        static {
            System.out.println("LazyHolder init");
        }
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance(){
        return LazyHolder.INSTANCE;
    }
}

结果:
Singleton init
LazyHolder init
cn.zyj.bytecode.Singleton@1b6d3586

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

5. 类加载器★★★★★

以 JDK 8 为例:

( BootStrapt ClassLoader ) 启动类加载器(C++ 写的所以无法直接访问)
( Extension ClassLoader ) 扩展类加载器
( Application ClassLoader ) 应用程序类加载器

双亲委派加载模式
  他们之间有一个层级关系,Application ClassLoader 加载类的时候首先会问一问看这个类,是否由他的上级加载过了,他就会问上级 Extension ClassLoader 看它有没有加载过这个类,如果没有呢么他还会让 Extension ClassLoader 在委托他的上上级 BootStrapt ClassLoader 看看是否加载过这个类,如果都没有加载过才轮到 Application ClassLoader 去加载这个类。

5.1 启动类加载器

用 BootStrapt 类加载器加载类:

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

执行:

public class Load4 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.zyj.bytecode.F");
        System.out.println(aClass.getClassLoader());//获取类加载器
    }
}

输出

H:\WorkSpace\AlgorithmSP\out\production\JVMStudy>java -Xbootclasspath/a:. cn.zyj.bytecode.Load4

bootstrap F init
null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclassoath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath:< new_bootclasspath>
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

5.2 扩展类加载器

public class G {//java文件1
    static {
        System.out.println("classpath G init");
    }
}
public class G {//java文件2
    static {
        System.out.println("ext G init");
    }
}

执行:

public class Load5 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.zyj.bytecode.G");
        System.out.println(aClass.getClassLoader());
    }
}
将java文件2 打包 jar -cvf name.jar cn\zyj\bytecode\G.class
并且将打包好的文件放入 \JAVA_HOME\jre\lib\ext
输出:
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

5.3 双亲委派模式

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

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

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1.检查该类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    	//2. 有上级的话,委派上级 loadClass
                        c = parent.loadClass(name, false);//递归
                    } else {
                    	//3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader 
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 4. 每一层都找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

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

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

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

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

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

  打印 null ,表示它的类加载器是BootstrapClassLoader ,会到 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;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
		//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();//每调用一次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() 就是应用程序类加载器(Application ClassLoader)
                Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

  DriverManager 本身是启动类加载器加载的,但是 ServiceLoader 内部由于用的是线程上下文类加载器呢他每调用一次 next 实际上内部就是用线程上下文类加载器完成了类加载,也是破坏了双亲委派的机制,并没有用启动类加载器去找 mysql 驱动。

  再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
  约定如下,在jar包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

这样就可以使用

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

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

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;//cn 是上一部分代码中的 cl
            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
        }

  DriverManager 本身是启动类加载器加载的,但是 ServiceLoader 内部由于用的是线程上下文类加载器呢他每调用一次 next 实际上内部就是用线程上下文类加载器完成了类加载,也是破坏了双亲委派的机制,并没有用启动类加载器去找 mysql 驱动。

5.5 自定义类加载器

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

  • 1)想加载非classpath随意路径中的类文件
  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    • 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

示例:
准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.loadClass("MapImpl1");
    }
}
class MyClassLoader extends ClassLoader{
    @Override // name 就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "e:\\myclasspath\\" + name + ".class";
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path),os);
            // 得到字节数组
            byte[] bytes = os.toByteArray();

            //byte[] -> *.class
            return defineClass(name,bytes,0,bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到",e);
        }
    }
}

6运行期优化

6.1 即时编译

分层编译(TieredCompilation)

例子

public class JIT1 {
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n", i, (end - start));
        }
    }
}
结果:
0	40100
1	20200
2	18300
3	18100
4	17100
...
65	10200
66	6400
67	10200
68	9800
69	9800
70	8600
...
141	10700
142	46500
143	29500
144	600
145	600
146	500
...
161	600
162	600
163	600
164	300
165	300
166	200

原因是什么呢?
JVM 将执行状态分成了 5个层次:

  • 0 层,解释执行(Interpreter)
  • 1层,使用C1即时编译器编译执行(不带 profiling )
  • 2层,使用C1即时编译器编译执行(带基本的 profiling )
  • 3层,使用C1即时编译器编译执行(带完全的 profiling )
  • 4层,使用C2即时编译器编译执行

  profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT会根据平台类型,生成平台特定的机器码

  对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下 Interpreter <C1<C2,总的目标是发现热点代码(hotspot名称的由来),优化之。

  刚才的一种优化手段称之为【逃逸分析】, 发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果发现到最后还是:10000左右的时间(ns)

  逃逸分析(C2编译器的优化):分析创建的对象会不会在循环外被使用到,被其他方法引用,结果发现不会,他就是循环内的局部变量,所以就采用了这样子的优化手段(发现创建对象的操作不会逃逸,说明外层不会用到此对象,把对象创建的字节码替换掉)

  • 栈上分配(Stack Allocation)
  • 同步消除(Synchronization Elimination)
  • 标量替换(Scalar Replacement)
方法内联(Inlining)
private static int square(final int i){
	return i * i;
}
System.out.println(square(9));

  如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:

System.out.println(9 * 9);

  还能够进行常量折叠(constant folding)的优化

System.out.println(81);
public class JIT2 {
    public static void main(String[] args) {
        int x = 0;
        for (int i = 0; i < 500; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n", i, x,(end - start));
        }
    }
    private static int square(int i) { return i * i; }
}
结果:
0	81	34400
1	81	28000
2	81	18400
...
497	81	0
498	81	0
499	81	100
字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/
创建 maven 工程,添加依赖如下

@Warmup(iteration = 2, time = 1)
@Measurement(iteration = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark {
    volatile int[] elements = randomInts(1_100);

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

    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }
    static int sum = 0;
    static void doSum(int x){ sum += x;}

    public static void main(String[] args) {
        Option opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

结果(允许 doSum 方法内联):

结果(不允许 doSum 方法内联):

Units(每秒执行操作,越高越好)
Score error(得分误差)
Score(吞吐量的得分)

分析:
  在刚才的示例中,doSum方法是否内联会影响 elements 成员变量读取的优化:
  如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):

可以节省1999次 Field 读取操作
但如果 doSum 方法没有内联,则不会进行上面的优化

6.2 反射优化

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

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

  foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorlmpl 实现

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        //使用的是 MethodAccessor
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

NativeMethodAccessorlmpl

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    	//计数并且计数值超过 private static int inflationThreshold = 15; 就会使用
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        	//将本地方法访问器替换成一个由运行期间动态生成的一个新的方法访问器
        	//会根据当前调用方法的一些信息,生成新的方法访问器
        	//方法的声明类、方法的名称、方法的类型、方法的返回值类型、方法的异常类型、方法的修饰符
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            //替换掉了原本的 NativeMethodAccessor
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }
	//使用了本地方法调用方法 在这种效率非常低
    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

调试查看生成的新的方法访问器的类名在( MethodAccessorImpl var3 = …)处设断点得到:class sun.reflect.GeneratedMethodAccessor1

使用 Alibaba 的arthas 查看

C:\Users\11218\Desktop> java -jar arthas-boot.jar


输入help查询命令

jad 为反编译命令(Decompile Class)

jad sun.reflect.GeneratedMethodAccessor1
package sun.reflect;

import cn.zyj.bytecode.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1
extends MethodAccessorImpl {
    /*
     * Loose catch block
     * Enabled aggressive block sorting
     * Enabled unnecessary exception pruning
     * Enabled aggressive exception aggregation
     * Lifted jumps to return sites
     */
    public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
    	//比较奇葩的做法,如果有参数,那么抛非法异常
        block4: {
            if (arrobject == null || arrobject.length == 0) break block4;
            throw new IllegalArgumentException();
        }
        try {
        	//可以看到,已经是直接调用了
            Reflect1.foo();
            //因为没有返回值,所以返回null
            return null;
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(super.toString());
        }
    }
}
Affect(row-cnt:1) cost in 440 ms.

注意
通过查看 ReflectionFactory 源码可知

  • sun.reflect.nolnflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
  • sun.reflect.iiiflationTiireshold 可以修改膨胀阈值
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值