天天看点

JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化

JVM-类加载与字节码技术

  • 类结构文件
    • magic
    • 版本
    • 常量池
  • javap工具
    • 图解方法执行流程
    • 条件判断指令
    • 构造方法
      • cinit 构造方法
      • init 构造方法
    • 方法调用
    • 多态的原理
    • synchronized
  • 编译期处理
    • 默认构造器
    • 自动拆装箱
    • 泛型集合取值
    • 可变参数
    • foreach 循环
      • 数组循环
      • 集合循环
    • switch 字符串
    • try-with-resources
    • 方法重写时的桥接方法
    • 匿名内部类
      • 无引用局部变量
      • 引用局部变量
  • 类加载阶段
    • 加载
    • 链接
      • 验证
      • 准备
      • 解析
    • 初始化
      • 发生的时机
  • 运行期优化
    • 及时编译
      • 分层编译
      • 方法内联
      • 字段优化
    • 反射优化
JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化

类结构文件

  • java程序
// HelloWorld 示例
public class HelloWorld {
	public static void main(String[] args) {
	System.out.println("hello world");
	}
}
           
  • 执行 javac -parameters -d . HellowWorld.java,编译为 HelloWorld.class 后是这个样子的:
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
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
           
  • 根据 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];
}
           

magic

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

版本

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

常量池

JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • 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 信息,查上述表发发现是一个方法引用的信息。CONSTANT_Methodref
    • 00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】。

省略

javap工具

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

图解方法执行流程

  • 原始java文件
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
	int a = 10;   //比较小的数值,例如10,并不是存储在常量池中,而是和方法的字节码指令存在一起
	int b = Short.MAX_VALUE + 1; //数值的范围超过了整数的最大值,会存储在常量池中
	int c = a + b;
	System.out.println(c);
	}
}
           
  • 字节码文件
Classfile /E:/studyData/JVM/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2021-3-11; 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
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // 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
}
           
  1. 常量池载入运行时常量池
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  2. 方法字节码载入方法区
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  3. main 线程开始运行,分配栈帧内存
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • 蓝色:操作数栈;绿色:局部变量表。在字节码文件中,定义了其大小(stack=2,locals=4)
  1. 执行引擎开始执行字节码
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
    • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
    • ldc 将一个 int 压入操作数栈
    • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
    • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
  • istore_1:将操作数栈顶数据弹出,存入局部变量表的 slot 1
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • ldc #3:从常量池加载 #3 数据到操作数栈
    • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的。
      JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • getstatic #4
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • invokevirtual #5
    • 找到常量池 #5 项
    • 定位到方法区 java/io/PrintStream.println:(I)V 方法
    • 生成新的栈帧(分配 locals、stack等)
    • 传递参数,执行新栈帧中的字节码。
    • 注意:传递参数实际上是将参数传递到println栈帧的操作数栈中。然后在这个栈帧中进行调用。
  • JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • 执行完毕,弹出栈帧
    • 清除 main 操作数栈内容
      JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • return
    • 完成 main 方法调用,弹出 main 栈帧
    • 程序结束

条件判断指令

JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
  • 几点说明:
    • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
    • goto 用来进行跳转到指定行号的字节码
    • 以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

构造方法

cinit 构造方法

  • java
public class Demo3_8_1 {
	static int i = 10;
	static {
		i = 20;
	}
	static {
		i = 30;
	}
}
           
  • 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 < cinit >()V
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化

init 构造方法

  • java
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);
        System.out.println(d.b);
    }
}
           
  • 编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化

方法调用

  • java
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();
        d.test2();
        d.test3();
        d.test4();
        Demo3_9.test4();
        d.toString();
    }
}
           
  • 字节码
    JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化
    • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
    • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】。为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “”😦)V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
    • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定。在编译的时候就确定的知道调用的哪个方法,直接可找到代码的执行地址。
    • 普通成员方法是由 invokevirtual 调用,属于动态绑定,在运行的时候才知道调用的哪个方法。即支持多态成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
    • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic (也属于静态绑定)之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了。因为静态方法不需要对象,是通过类名。
    • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

多态的原理

  • java
/**
 * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩。因为64位虚拟机为了节省内存空间,使用了指针压缩技术,查看内存地址时需要进行地址变换。
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointe	rs
 */
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("吃鱼");
    }
}

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

synchronized

  • java
public class Demo3_13 {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
           
  • 字节码文件
Classfile /E:/studyData/JVM/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_13.class
  Last modified 2021-3-11; size 735 bytes
  MD5 checksum 2f9da4c55f7511811c01df8263d5fe33
  Compiled from "Demo3_13.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_13
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#26         // java/lang/Object."<init>":()V
   #2 = Class              #27            // java/lang/Object
   #3 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #30            // ok
   #5 = Methodref          #31.#32        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #33            // cn/itcast/jvm/t3/bytecode/Demo3_13
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_13;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               lock
  #19 = Utf8               Ljava/lang/Object;
  #20 = Utf8               StackMapTable
  #21 = Class              #17            // "[Ljava/lang/String;"
  #22 = Class              #27            // java/lang/Object
  #23 = Class              #34            // java/lang/Throwable
  #24 = Utf8               SourceFile
  #25 = Utf8               Demo3_13.java
  #26 = NameAndType        #7:#8          // "<init>":()V
  #27 = Utf8               java/lang/Object
  #28 = Class              #35            // java/lang/System
  #29 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #30 = Utf8               ok
  #31 = Class              #38            // java/io/PrintStream
  #32 = NameAndType        #39:#40        // println:(Ljava/lang/String;)V
  #33 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_13
  #34 = Utf8               java/lang/Throwable
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (Ljava/lang/String;)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_13();
    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/itcast/jvm/t3/bytecode/Demo3_13;

  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                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 12
        line 9: 20
        line 10: 30
      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
}
SourceFile: "Demo3_13.java"
           
方法级别的 synchronized 不会在字节码指令中有所体现
           

编译期处理

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

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

默认构造器

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

自动拆装箱

  • 这个特性是 JDK 5 开始加入的。
public class Candy2 {
	public static void main(String[] args) {
		Integer x = 1;
		int y = x;
	}
}
           
  • 上述代码在 JDK 5 之前是无法编译通过的,必须改写
public class Candy2 {
	public static void main(String[] args) {
		Integer x = Integer.valueOf(1);
		int y = x.intValue();
	}
}
           

泛型集合取值

  • 泛型也是在 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();
           

可变参数

  • 可变参数也是 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);
		}
	}
}
           

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

try-with-resources

  • JDK 7 开始新增了对需要关闭的资源处理的特殊语法try-with-resources
try(资源变量 = 创建资源对象){
} catch( ) {
}
           
  • 其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、OutputStream 、Connection 、Statement 、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 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();
		}
	}
}
           
  • 编译器转换后的代码会捕获异常,通过finally进行资源的关闭

方法重写时的桥接方法

  • 方法重写时对返回值分两种情况:
    • 父子类的返回值完全一致
    • 子类返回值可以是父类返回值的子类,详细看例子。
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() 没有命名冲突

匿名内部类

无引用局部变量

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

引用局部变量

  • java
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 的。因为当x发生改变时,会导致原始类中局部变量会跟额外生成的类中的属性不一致。
 因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val&$x属性所有x不应该再发生变化。
 如果变化,那么val$x属性也不会再跟着一起变化。 
           

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
    • _java_mirror 即 java 的类镜像。例如对 String 来说,类镜像就是 String.class,作用是把 klass 暴露给 java 使用。可以理解为是C++与java之间的桥梁。
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
  • 注意:
    • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
    • 可以通过前面介绍的 HSDB 工具查看
      JVM-类加载与字节码技术类结构文件javap工具编译期处理类加载阶段运行期优化

链接

验证

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

准备

  • 为 static 变量分配空间,设置默认值
    • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,也就是存储在方法区中。从 JDK 7 开始,存储于_java_mirror 末尾,也就是存储在堆中。
    • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 注意事项:如上所示,静态变量在JDK7之前存储在方法区,在这之后存储在堆中。某些教材并没有做区别。

解析

  • 将常量池中的符号引用解析为直接引用。
  • 符号引用:并不知道这符号到底对应的哪个内存地址,仅仅只是符号而已。
/**
 * 解析的含义
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
       	ClassLoader classloader = Load2.class.getClassLoader();
        //只涉及类的加载,不会触发解析以及初始化
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        //new C();    //会触发C的加载,解析,初始化过程。因此会加载类D
        System.in.read();
    }
}

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

class D {

}
           

初始化

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

发生的时机

  • 概括得说,类初始化是【懒惰的】
    • main 方法所在的类,总会被首先初始化
    • 首次访问这个类的静态变量或静态方法时
    • 子类初始化时,如果父类还没初始化,会触发父类的初始化。
    • 子类访问父类的静态变量,只会触发父类的初始化
    • 执行Class.forName时,会导致类的初始化。执行过程:JVM会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,会对类进行初始化,即执行类的静态代码块。
    • new 会导致初始化
  • 不会导致类初始化的情况
    • 访问类的 static final **静态常量(基本类型和字符串)**不会触发初始化
    • 类对象.class 不会触发初始化。执行过程:执行类名.class时,JVM会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,不会对Class对象进行初始化。
    • 创建该类的数组不会触发初始化
    • 类加载器的loadClass方法
    • Class.forName的参数2为false
  • 练习:懒惰初始化的单例模式
    • 实现特点是:
      • 懒惰实例化
      • 初始化时的线程安全是有保障的
public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();
        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;
    }
}

           

运行期优化

及时编译

分层编译

  • 代码演示:从运行结果可以看出,循环创建对象后,消耗的时间变小。
public class JIT1 {
	// 进行逃逸分析的配置
    // -XX:+PrintCompilation -XX:-DoEscapeAnalysis
    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 会根据平台类型,生成平台特定的机器码
  • JVM采用的策略:对于占据大部分的不常用的代码,无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之。在上述的示例中,所使用的优化手段为

    逃逸分析

    ,需要重点理解。

方法内联

  • 重点理解

字段优化

  • 即时编译器会优化实例字段和静态字段的访问,以减少总的内存访问次数
  • 即时编译器将沿着控制流,缓存各个字段存储节点将要存储的值,或者字段读取节点所得到的值
    • 当即时编译器遇到对同一字段的读取节点时,如果缓存值还没有失效,那么将读取节点替换为该缓存值
    • 当即时编译器遇到对同一字段的存储节点时,会更新所缓存的值
      • 当即时编译器遇到可能更新字段的节点时,它会采取保守的策略,舍弃所有的缓存值
      • 方法调用节点:在即时编译器看来,方法调用会执行未知代码
      • 内存屏障节点:其他线程可能异步更新了字段

反射优化

  • java
public class Reflect1 {

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

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        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 实现,为本地方法,执行费时。
  • 默认执行次数大于15后,会生成一个新的方法访问器类,代替掉最初的实现。