黑马程序员JVM笔记03-类加载与字节码技术

本文详细探讨了Java虚拟机(JVM)的类文件结构,包括魔数、版本号、常量池等组成部分,并介绍了字节码指令的分析,使用Javap工具进行反编译。文章通过实例展示了方法执行流程,讲解了构造方法、方法调用、多态原理等。此外,还涉及了类加载的各个阶段,包括加载、链接(验证、准备、解析)、初始化,以及类加载器的工作机制。最后,讨论了运行期的即时编译(JIT)优化,如分层编译、方法内联和字段优化等技术。
摘要由CSDN通过智能技术生成

在这里插入图片描述

类文件结构

以一段简单的java代码为例:

package cn.itcast.jvm.t5;
// HelloWorld 示例
public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("hello world");
 }
}

执行 javac -parameters -d . HellowWorld.java
编译后的Class文件(二进制):
在这里插入图片描述
根据JVM规范,类文件结构如下:

ClassFile {
	 u4       magic;
	 u2       minor_version;
	 u2       major_version;
	 u2       constant_pool_count;
	 cp_info    constant_pool[constant_pool_count-1];
	 u2       access_flags;
	 u2       this_class;
	 u2       super_class;
	 u2       interfaces_count;
	 u2       interfaces[interfaces_count];
	 u2       fields_count;
	 field_info   fields[fields_count];
	 u2       methods_count;
	 method_info  methods[methods_count];
	 u2       attributes_count;
	 attribute_info attributes[attributes_count];
}

1. 魔数

0~3 字节,表示它是否是【class】类型的文件
在这里插入图片描述

2. 版本号

4~7 字节,表示类的版本
00 34(52) 表示是 Java 8
在这里插入图片描述

3. 常量池

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. 访问标识与继承信息

在这里插入图片描述
在这里插入图片描述

5. Field信息

在这里插入图片描述
在这里插入图片描述

6. Method信息

表示方法数量,本类为 2
在这里插入图片描述
一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

分析第一个方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
分析第二个方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7. 附加属性

在这里插入图片描述

参考文献

字节码指令

分析两个方法

接着上一节,研究一下两组字节码指令
一个是public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

在这里插入图片描述

  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?
  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】
  4. b1 表示返回

另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

在这里插入图片描述

  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?
  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  3. 12 => ldc 加载参数,哪个参数呢?
  4. 03 引用常量池中 #3 项,即 【String hello world】
  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?
  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  7. b1 表示返回

请参考查阅字节码含义

Javap工具

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图解方法执行流程

  1. 原始 java 代码
package cn.itcast.jvm.t3.bytecode;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
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);
 }
}
  1. 编译后的字节码文件

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 常量池载入运行时常量池
    在这里插入图片描述

  2. 方法字节码载入方法区
    在这里插入图片描述

  3. main 线程开始运行,分配栈帧内存
    (stack=2,locals=4)

在这里插入图片描述
6. 执行引擎开始执行字节码

bipush 10

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

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1
  • istore_1中的1就是指slot槽位号
    在这里插入图片描述
    在这里插入图片描述

ldc #3

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

istore_2

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 2
    在这里插入图片描述
    在这里插入图片描述

iload_1
在这里插入图片描述

iload_2
在这里插入图片描述

iadd
在这里插入图片描述
在这里插入图片描述

istore_3
在这里插入图片描述

在这里插入图片描述
getstatic #4
在这里插入图片描述
在这里插入图片描述

iload_3
在这里插入图片描述
在这里插入图片描述
invokevirtual #5

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码
    在这里插入图片描述
  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容
    在这里插入图片描述

return

  • 完成 main 方法调用,弹出 main 栈帧
  • 程序结束

练习:分析i++

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

package cn.itcast.jvm.t3.bytecode;
/**
* 从字节码角度分析 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);
 }
}

在这里插入图片描述

在这里插入图片描述

分析:

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

条件判断指令

在这里插入图片描述

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码

源码:

public class Demo3_3 {
  public static void main(String[] args) {
    int a = 0;
    if(a == 0) {
      a = 10;
   } else {
      a = 20;
   }
 }
}

字节码:
在这里插入图片描述

循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

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

字节码:
在这里插入图片描述
再比如 do while 循环:

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

字节码:
在这里插入图片描述
最后再看看 for 循环:

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

字节码:
在这里插入图片描述

注意
比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归😊

练习:判断结果

请从字节码角度分析,下列代码运行的结果:

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

构造方法

  1. <cinit>()V
public class Demo3_8_1 {
  static int i = 10;
  static {
    i = 20;
 }
  static {
    i = 30;
 }
}

编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方
<cinit>()V
在这里插入图片描述

<cinit>()V 方法会在类加载的初始化阶段被调用
可以自己调整一下 static 变量和静态代码块的位置,观察字节码的改动

  1. <init>()V
public class Demo3_8_2 {
  private String a = "s1";
 {
    b = 20;
 }
  private int b = 10;
 {
    a = "s2";
 }
   public Demo3_8_2(String a, int b) {
    this.a = a;
    this.b = b;
 }
  public static void main(String[] args) {
    Demo3_8_2 d = new Demo3_8_2("s3", 30);
    System.out.println(d.a);	//结果:s3
    System.out.println(d.b);	//结果:30
 }
}

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

方法调用

看一下几种不同的方法调用对应的字节码指令

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

字节码:
在这里插入图片描述

  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V(会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了😂
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

多态的原理

package cn.itcast.jvm.t3.bytecode;

import java.io.IOException;

/**
 * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
 */
public class Demo3_10 {

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

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

abstract class Animal {
    public abstract void eat();

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

class Dog extends Animal {

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

class Cat extends Animal {

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

  1. 运行代码
    演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
    -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
    停在 System.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 d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行
    在这里插入图片描述

  4. 查看对象内存结构
    点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针
    但目前看不到它的实际地址
    在这里插入图片描述

  5. 查看对象 Class 的内存地址
    可以通过 Windows -> Console 进入命令行模式,执行

mem 0x00000001299b4978 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
结果中第二行0x000000001b7d4028即为 Class 的内存地址
在这里插入图片描述

  1. 查看类的 vtable
    方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面
    在这里插入图片描述
    方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果
    在这里插入图片描述
    无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,final,static 不会列入)
    那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:

在这里插入图片描述

通过 Windows -> Console 进入命令行模式,执行

mem 0x000000001b7d41e0 6

0x000000001b7d41e0: 0x000000001b3d1b10
0x000000001b7d41e8: 0x000000001b3d15e8
0x000000001b7d41f0: 0x000000001b7d35e8
0x000000001b7d41f8: 0x000000001b3d1540
0x000000001b7d4200: 0x000000001b3d1678
0x000000001b7d4208: 0x000000001b7d3fa8

就得到了 6 个虚方法的入口地址

  1. 验证方法地址
    通过 Tools -> Class Browser 查看每个类的方法定义,比较可知
    在这里插入图片描述
    对号入座,发现
  • eat() 方法是 Dog 类自己的
  • toString() 方法是继承 String 类的
  • finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
  1. 小结
    当执行 invokevirtual 指令时,
  • 先通过栈帧中的对象引用找到对象
  • 分析对象头,找到对象的实际 Class
  • Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  • 查表得到方法的具体地址
  • 执行方法的字节码

异常处理

  1. try-catch
public class Demo3_11_1 {
  public static void main(String[] args) {
    int i = 0;
    try {
      i = 10;
   } catch (Exception e) {
      i = 20;
   }
 }
}

注意
为了抓住重点,下面的字节码省略了不重要的部分

在这里插入图片描述
可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

  1. 多个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;
   }
 }
}

在这里插入图片描述
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表slot 2 位置被共用

  1. multi-catch 的情况
public class Demo3_11_3 {
finally
  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");
 }
}

在这里插入图片描述

  1. 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;
   }
 }
}

在这里插入图片描述
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

finally面试题

finally 出现了 return
先问问自己,下面的题目输出什么?

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

在这里插入图片描述

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally 的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally中出现了 return,会吞掉异常😱😱😱,可以试一下下面的代码
public class Demo3_12_1 {
  public static void main(String[] args) {
    int result = test();
    System.out.println(result);
 }
  public static int test() {
    try {
      int i = 1/0;
      return 10;
   } finally {
      return 20;
   }
 }
}

finally 对返回值影响
同样问问自己,下面的题目输出什么?

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

在这里插入图片描述

synchronized

public class Demo3_13 {
  public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
      System.out.println("ok");
   }
 }  
}

在这里插入图片描述

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

编译期处理

所谓的语法糖,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记

默认构造器

public class Candy1 {
}

编译成Class后的代码:

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

自动拆装箱

这个特性是 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();
 }
}

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

泛型集合取值

泛型也是在 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 Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
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]);
   }
 }
}

输出:
在这里插入图片描述

可变参数

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

public class Candy4 {
  public static void foo(String... args) {
    String[] array = args; // 直接赋值
    System.out.println(array);
 }
  public static void main(String[] args) {
    foo("hello", "world");
 }
}

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

public class Candy4 {
  public static void foo(String[] args) {
    String[] array = args; // 直接赋值
    System.out.println(array);
 }
  public static void main(String[] args) {
    foo(new String[]{"hello", "world"});
 }
}

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

foreach 循环

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

public class Candy5_1 {
  public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
    for (int e : array) {
      System.out.println(e);
   }
 }
}

会被编译器转换为:

public class Candy5_1 {
  public Candy5_1() {
 }
  public static void main(String[] args) {
    int[] array = new int[]{1, 2, 3, 4, 5};
    for(int i = 0; i < array.length; ++i) {
      int e = array[i];
      System.out.println(e);
   }
 }
}

而集合的循环:

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

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

public class Candy5_2 {
  public Candy5_2() {
 }
  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    Iterator iter = list.iterator();
    while(iter.hasNext()) {
      Integer e = (Integer)iter.next();
      System.out.println(e);
   }
 }
}

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

switch字符串

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

public class Candy6_1 {
  public static void choose(String str) {
    switch (str) {
      case "hello": {
        System.out.println("h");
        break;
     }
      case "world": {
        System.out.println("w");
        break;
     }
   }
 }
}

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

会被编译器转换为:

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

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

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的hashCode值都是2123 ,如果有如下代码:

public class Candy6_2 {
  public static void choose(String str) {
    switch (str) {
      case "BM": {
        System.out.println("h");
        break;
     }
      case "C.": {
        System.out.println("w");
        break;
     }
   }
 }
}

会被编译器转换为:

public class Candy6_2 {
  public Candy6_2() {
 }
  public static void choose(String str) {
    byte x = -1;
    switch(str.hashCode()) {
    case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
      if (str.equals("C.")) {
        x = 1;
     } else if (str.equals("BM")) {
        x = 0;
     }
    default:
      switch(x) {
      case 0:
        System.out.println("h");
        break;
      case 1:
        System.out.println("w");
     }
   }
 }
}

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

枚举类

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};
 }
  /**
  * Sole constructor. Programmers cannot invoke this constructor.
  * It is for use by code emitted by the compiler in response to
  * enum type declarations.
  *
  * @param name  - The name of this enum constant, which is the identifier
  *        used to declare it.
  * @param ordinal - The ordinal of this enumeration constant (its position
  *        in the enum declaration, where the initial constant is
assigned
  */
  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);
 }
}

try-with-resources

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

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

} catch( ) {

}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了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 生成的 fianlly 中如果抛出了异常):

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

输出:
在这里插入图片描述
如以上代码所示,两个异常信息都不会丢。

方法重写时的桥接方法

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

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
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 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$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化

类加载阶段

1. 加载

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

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

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

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

在这里插入图片描述

2. 链接

2.1 验证

验证类是否符合 JVM规范,安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
在这里插入图片描述

2.2 准备

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

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
// 演示 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();
}

2.1 解析

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

package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
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 会进行类的解析和初始化
    new C();
    System.in.read();
 }
}
class C {
  D d = new D();
}
class D {
}

3. 初始化

初始化即调用 ()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");
    }
}

面试题

  1. 从字节码分析,使用 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";
  //Integer是引用类型,会导致E的初始化
  public static final Integer c = 20;	//Integer.valueOf(20)
}
  1. 典型应用 - 完成懒惰初始化单例模式
public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();	//不会初始化Singleton
        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;
    }
}

以上的实现特点是:

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

类加载器

以 JDK 8 为例:
在这里插入图片描述

启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;
public class F {
  static {
    System.out.println("bootstrap F init");
 }
}

执行

package cn.itcast.jvm.t3.load;
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());
 }
}

输出:启动类加载器不能被Java直接访问,所以才输出null
在这里插入图片描述

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

扩展类加载器

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

执行

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

输出
在这里插入图片描述
写一个同名的类

package cn.itcast.jvm.t3.load;
public class G {
  static {
    System.out.println("ext G init");
 }
}

打个 jar 包
在这里插入图片描述
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
重新执行 Load5_2
输出
在这里插入图片描述

双亲委派模式

所谓的双亲委派,就是指调用类加载器的 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) {
     }
    
      if (c == null) {
        long t1 = System.nanoTime();
        // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
        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;
 }
}

调试以下代码:

public class Load5_3 {
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> aClass = Load5_3.class.getClassLoader()
     .loadClass("cn.itcast.jvm.t3.load.H");
    System.out.println(aClass.getClassLoader());
 }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级sun.misc.Launcher$ExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派BootstrapClassLoader查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到sun.misc.Launcher$AppClassLoader的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的findClass 方法,在classpath 下查找,找到了

线程上下文类加载器

我们在使用 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,表示它的类加载器是 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 包下,以接口全限定名名为文件,文件内容是实现类名称
在这里插入图片描述
这样就可以使用

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

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

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

自定义类加载器

问问自己,什么时候需要自定义类加载器
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();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);	//相同的类加载器:true

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3);	//不同的类加载器:false

        c1.newInstance();
    }
}

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

在这里插入图片描述
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关闭逃逸分析,再运行刚才的示例观察结果

方法内联

(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 {
  // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印
inlining 信息
  // -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
  // -XX:+PrintCompilation 打印编译信息
  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(final int i) {
    return i * i;
 }
}

字段优化

JMH 基准测试参考

创建 maven 工程,添加依赖如下

<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-core</artifactId>
	<version>${jmh.version}</version>
</dependency>
<dependency>
	<groupId>org.openjdk.jmh</groupId>
	<artifactId>jmh-generator-annprocess</artifactId>
	<version>${jmh.version}</version>
	<scope>provided</scope>
</dependency>

编写基准测试代码:

package test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {
  int[] elements = randomInts(1_000);
  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;
  @CompilerControl(CompilerControl.Mode.INLINE)
  static void doSum(int x) {
    sum += x;
 }
  public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
       .include(Benchmark1.class.getSimpleName())
       .forks(1)
       .build();
    new Runner(opt).run();
 }
}

首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好):
在这里插入图片描述
接下来禁用 doSum 方法内联

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
  sum += x;
}

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

@Benchmark
public void test1() {
  // elements.length 首次读取会缓存起来 -> int[] local
  for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
    sum += elements[i]; // 1000 次取下标 i 的元素 <- local
 }
}

可以节省 1999 次 Field 读取操作
但如果 doSum 方法没有内联,则不会进行上面的优化
练习:在内联情况下将 elements 添加 volatile 修饰符,观察测试结果

6.2反射优化

package cn.itcast.jvm.t3.reflect;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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 的NativeMethodAccessorImpl 实现

package sun.reflect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;
class NativeMethodAccessorImpl extends MethodAccessorImpl {
  private final Method method;
  private DelegatingMethodAccessorImpl parent;
  private int numInvocations;
  NativeMethodAccessorImpl(Method method) {
    this.method = method;
 }
  public Object invoke(Object target, Object[] args)
    throws IllegalArgumentException, InvocationTargetException {
    // inflationThreshold 膨胀阈值,默认 15
    if (++this.numInvocations > ReflectionFactory.inflationThreshold()
      && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass()))
{
      // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
      MethodAccessorImpl generatedMethodAccessor =
       (MethodAccessorImpl)
         (new MethodAccessorGenerator())
           .generateMethod(
              this.method.getDeclaringClass(),
              this.method.getName(),
              this.method.getParameterTypes(),
              this.method.getReturnType(),
              this.method.getExceptionTypes(),
              this.method.getModifiers()
           );
      this.parent.setDelegate(generatedMethodAccessor);
   }
    // 调用本地实现
    return invoke0(this.method, target, args);
 }
  void setParent(DelegatingMethodAccessorImpl parent) {
    this.parent = parent;
 }
  private static native Object invoke0(Method method, Object target, Object[] args);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为sun.reflect.GeneratedMethodAccessor1

可以使用阿里的 arthas 工具:

在这里插入图片描述
选择 1 回车表示分析该进程
在这里插入图片描述
再输入【jad + 类名】来进行反编译:
在这里插入图片描述
在这里插入图片描述

注意
通过查看 ReflectionFactory 源码可知
sun.reflect.noInflation 可以用来禁用膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
sun.reflect.inflationThreshold 可以修改膨胀阈值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值