JVM3 类加载与字节码技术

在这里插入图片描述

一、类文件结构

源代码

public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("hello world");
	}
}
  • 执行 javac -parameters -d . HellowWorld.java

字节码(16进制)

[root@localhost ~]# od -t xC 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];
}

1、魔数

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • 0~3 字节,表示它是否是【class】类型的文件

2、版本

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • 4~7 字节,表示类的版本 00 34(52) 表示是 Java 8

3、常量池

  • 常量池表
Constant TypeValue
CONSTANT_Class7
CONSTANT_Fieldref9
CONSTANT_Methodref10
CONSTANT_InterfaceMethodref11
CONSTANT_String8
CONSTANT_Integer3
CONSTANT_Float4
CONSTANT_Long5
CONSTANT_Double6
CONSTANT_NameAndType12
CONSTANT_Utf81
CONSTANT_MethodHandle15
CONSTANT_MethodType16
CONSTANT_InvokeDynamic18

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  • 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 项来获得这个方法的【所属类】和【方法名】

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

  • 第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

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

  • 第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

  • 第#21项 0c 表示一个 【名+类型】,00 07 00 08 引用了常量池中 #7 #8 两项

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  • 第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【 构 造 方 法 】

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

  • L第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值

4、访问标识与继承信息

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  • 21 表示该 class 是一个类,公共的

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  • 05 表示根据常量池中 #5 找到本类全限定名

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  • 06 表示根据常量池中 #6 找到父类全限定名

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  • 表示接口的数量,本类为 0

5、Field 信息

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  • 表示成员变量数量,本类为 0

6、Method 信息

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

  • 表示方法数量,本类为 2

一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

在这里插入图片描述

  • 红色代表访问修饰符(本类中是 public)
  • 蓝色代表引用了常量池 #07 项作为方法名称
  • 绿色代表引用了常量池 #08 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 1
  • 红色代表方法属性
    • 00 09 表示引用了常量池 #09 项,发现是【Code】属性
    • 00 00 00 2f 表示此属性的长度是 47
    • 00 01 表示【操作数栈】最大深度
    • 00 01 表示【局部变量表】最大槽(slot)数
    • 00 00 00 05 表示字节码长度,本例是 5
    • 2a b7 00 01 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
      • 00 00 00 06 表示此属性的总长度,本例是 6
      • 00 01 表示【LineNumberTable】长度
      • 00 00 表示【字节码】行号 00 04 表示【java 源码】行号
    • 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
      • 00 00 00 0c 表示此属性的总长度,本例是 12
      • 00 01 表示【LocalVariableTable】长度
      • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
      • 00 05 表示局部变量覆盖的范围长度
      • 00 05 表示局部变量覆盖的范围长度
      • 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是【Lcn/itcast/jvm/t5/HelloWorld;】
      • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

在这里插入图片描述

  • 红色代表访问修饰符(本类中是 public static)
  • 蓝色代表引用了常量池 #14 项作为方法名称
  • 绿色代表引用了常量池 #15 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 2
  • 红色代表方法属性(属性1)
    在这里插入图片描述
  • 红色代表方法属性(属性2)
    • 00 12 表示引用了常量池 #18 项,发现是【MethodParameters】属性
      • 00 00 00 05 表示此属性的总长度,本例是 5
      • 01 参数数量
      • 00 10 表示引用了常量池 #16 项,是【args】
      • 00 00 访问修饰符

7、附加属性

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即【SourceFile】
  • 00 00 00 02 表示此属性的长度
  • 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】

8、 参考文献

二、字节码指令

1、入门

构造方法:2a b7 00 01 b1

  • 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
  • b7 => invokespecial 预备调用构造方法
  • 00 01 引用常量池中 #1 项即Object中的无参构造
  • b1 表示返回

主方法 b2 00 02 12 03 b6 00 04 b1

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

2、参考

3、javap 工具

  • javap -v HelloWorld.class
    在这里插入图片描述

4、方法执行流程

  • 原始Java代码 ->字节码文件
public class Demo3_1 {
	public static void main(String[] args) {
		int a = 10;
		int b = Short.MAX_VALUE + 1;
		int c = a + b;
		System.out.println(c);
	}
}
  • 类加载器读取编译后的字节码文件

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

  • 方法字节码载入方法区
    -

  • main 线程开始运行,分配栈帧内存
    在这里插入图片描述
    蓝色:操作数栈深度(stack=2),绿色:局部变量表(locals=4)

  • 执行引擎开始执行字节码

    • bipush10:将一个byte压入操作数栈(操作数栈为4个字节,长度会用0补齐)
      在这里插入图片描述

    • istore_1:将操作数栈顶数据弹出,存入局部变量表slot1(槽位1)
      在这里插入图片描述

    • ldc #3:从常量池加载#3数据到操作数栈(常量折叠)
      在这里插入图片描述

    • istore_2:将操作数栈顶数据弹出,存入局部变量表slot2(槽位2)
      在这里插入图片描述

    • iload_1,iLoad_2;读取局部变量表槽位1 2 数字并压入操作数栈
      在这里插入图片描述

    • iadd:加法运算
      在这里插入图片描述

    • istore_3:将操作数栈顶数据弹出,存入局部变量表slot3(槽位3)
      在这里插入图片描述

    • getstatic #4:常量池中找到成员变量引用并存入操作数栈
      在这里插入图片描述

    • iload_3:读取局部变量槽位3数字并压入操作数栈
      在这里插入图片描述

    • invokevirtual#5:找到常量池#5项–》 定位到println方法–》 生成新的栈帧给println–》 传递参数执行新栈帧中字节码–》 执行完毕弹出栈桢–》 清除main操作数栈内容
      在这里插入图片描述
      在这里插入图片描述

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

5、案例

  • 从字节码角度分析a++
    • a++先iload再iinc ++a先iinc再iload
      • Iinc(1,1):对于1号槽位+1

6、条件判断指令

在这里插入图片描述

  • byte,short,char按int比较,操作数栈都是4字节
  • goto用于跳转到指定行号的字节码
  • 比较小的数用iconst表示:-1到5
    源码
public static void main(String[] args) {
		int a = 0;
		if(a == 0) {
		a = 10;
		} else {
		a = 20;
	}
}

字节码

0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return

7、 循环控制指令

while源码

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

字节码

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 static void main(String[] args) {
	int a = 0;
	do {
		a++;
	} while (a < 10);
}

字节码

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

for源码

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

字节码

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字节码指令一样

8、构造方法

  • 1、< cinit >()V
    • 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 < cinit >()V
    • < cinit >()V 会在类的加载的初始化阶段被调用
  • 2、< init >()V
    • 编译器会按从上至下的顺序,收集所有初始化代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

9、方法调用

源码

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

字节码

0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "< init > " : ()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
  • d.test4()是通过【对象引用】调用一个静态方法,在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
    • 静态方法调用不需要对象,直接使用类名调用即可
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

10、多态原理

  • 参数
    • -XX:-UseCompressedOops
    • -XX:-UseCompressedClassPointers 禁用指针压缩
  • HSDB工具
    • jdk目录下-java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
  • HSDB查找步骤
    当执行 invokevirtual 指令时
    • 先通过栈帧中的对象引用找到对象
    • 分析对象头,找到对象的实际 Class
    • Class 结构中有 vtable(虚方法表),它在类加载的链接阶段就已经根据方法的重写 规则生成好了
    • 查表得到方法的具体地址
    • 执行方法的字节码

11、异常处理

  • try-catch

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

源代码

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

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

  • 多个single-catch快的情况
    • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用,节省栈帧内存

源代码

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

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

  • multi-catch 的情况(一个catch捕获多个异常)
    • 三个不同类型异常target入口相同

源代码

public class Demo3_11_3 {
	public static void main(String[] args) {
		try {
			Method test = Demo3_11_3.class.getMethod("test");
			test.invoke(null);
		} catch (NoSuchMethodException | IllegalAccessException |
		InvocationTargetException e) {
			e.printStackTrace();
		}
	}
	public static void test() {
		System.out.println("ok");
	}
}	

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

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

源代码

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面试题
    • finally 出现了 return
      • 返回结果以finally为准
      • 如果在finally中写入return,会吞掉异常
    • finally 对返回值影响
      • Try中有return语句,Finally中没有return语句,会正常抛出异常并返回try中的结果(暂存)

12、synchronized

源代码

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

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

  • Monitorenter:加锁指令 Monitorexit:解锁指令。
    • 因为有加锁解锁两个操作,会复制对象引用。
  • 方法级别的 synchronized 不会在字节码指令中有所体现

三、编译期处理

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

1、默认构造器

源代码

public class Candy1 {
}

编译为class后的代码

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

2、自动拆装箱

  • JDK5之后 编译器会在基本类型和包装类型之间的自动来回转换

3、泛型集合取值

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

源代码

public class Candy3 {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		list.add(10); // 实际调用的是 List.add(Object e)
		Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
	}
}
  • 在取值时,编译器真正生成的字节码中,还要额外做一个类型转换操作
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
  • 如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
  • 注意:擦除的是字节码上的泛型信息, LocalVariableTypeTable(局部变量类型表) 仍然保留了方法参数泛型的信息,使用反射仍然能够获得这些信息。
    在这里插入图片描述

4、可变参数

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

源代码

public class Candy4 {
	public static void foo(String... args) {
		String[] array = args; // 直接赋值
		System.out.println(array);
	}
	public static void main(String[] args) {
		foo("hello", "world");
	}
}
  • 可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。

编译期间

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 进去。

5、foreach

  • 数组赋初值的简化写法也是语法糖
  • JDK5加入的特性,遍历数组实际是fori的循环,在集合循环实际被编译器转换为对迭代器的调用
  • foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中Iterable 用来获取集合的迭代器( Iterator

6、switch

  • String
    • 从 JDK 7 开始,switch 可以作用于字符串和枚举类
    • switch 配合 String 和枚举使用时,变量不能为null

源代码

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

编译期

public class Candy6_1 {
	public Candy6_1() {
	}
	public static void choose(String str) {
		byte x = -1;
		switch(str.hashCode()) {
		case 99162322: // hello 的 hashCode
			if (str.equals("hello")) {
				x = 0;
			}
			break;
		case 113318802: // world 的 hashCode
			if (str.equals("world")) {
				x = 1;
			}
}
	switch(x) {
		case 0:
			System.out.println("h");
			break;
		case 1:
			System.out.println("w");
		}
	}
}
  • 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较。

    • hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C.
  • 枚举

源代码

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;

7、枚举类

  • JDk7新特性,实质为有限实例对象类

8、try-with-resources

  • JDK7新特性,简化资源关闭
    • 语法
    • 其中资源对象需要实现 AutoCloseable 接口,如 InputStream 、OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-with resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码。
try(资源变量 = 创建资源对象){ 
} catch( ) { 
} 

源代码

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)(添加被压制异常)的方法是为了防止异常信息的丢失

9、方法重写时的桥接方法

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

源代码

class A {
	public Number m() {
		return 1;
	}
}
class B extends A {
	@Override
	// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
	public Integer m() {
		return 2;
	}
}

编译期,子类处理

class B extends A {
	public Integer m() {
		return 2;
	}
	// 此方法才是真正重写了父类 public Number m() 方法
	public synthetic bridge Number m() {
		// 调用 public Integer m()
		return m();
	}
}
  • 可以看到对于第二种情况 编译器会在子类自动生一个符合方法重写规则的方法,间接调用原有的方法(桥接方法仅对Java虚拟机可用)。

10、匿名内部类

  • 引用局部变量的匿名内部类

源代码

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 的:因为在创建额外类的实例对象时,将局部变量的值赋值给了实例对象的对应属性,所以局部变量值不应该再发生变化了,如果变化,那么对应的属性没有机会再跟着一起变化

四、类加载阶段(加载-链接-初始化)

1、加载

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
    在这里插入图片描述
  • 实例对象通过_java_mirror存储的instancKlass地址进入元空间
    • _java_mirror:即java的镜像类,例如对String来说,就是String.class,作用是把klacss暴露给java使用
    • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
      • 可以通过 HSDB 工具查看

2、链接

  • 验证: 验证类(字节码)是否符合JVM规范,安全性检查
  • 准备: 为static变量分配空间,设置默认值
    • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
    • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析: 将常量池中的符号引用解析为直接引用
/**
 * 解析的含义
*/
public class Load2 {
	public static void main(String[] args) throws ClassNotFoundException,
IOException {
		ClassLoader classloader = Load2.class.getClassLoader();
		// loadClass 方法不会导致类的解析和初始化
		Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
		// new C();
		System.in.read();
	}
}
class C {
	D d = new D();
}
class D {
}

3、初始化

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

  • 发生的时机(类初始化是懒惰的)
    • main 方法所在的类,总会被首先初始化
    • 首次访问这个类的静态变量或静态方法时
    • 子类初始化,如果父类还没初始化,会引发父类初始化
    • 子类访问父类的静态变量,只会触发父类的初始化
    • Class.forName
    • new会导致初始化
  • 不导致类初始化的情况
    • 访问类的 static fifinal 静态常量(基本类型和字符串)不会触发初始化
    • 类对象.class 不会触发初始化(加载阶段)
    • 创建该类的数组不会触发初始化
    • 类加载器的 loadClass 方法
    • Class.forName 的参数 2 为 false 时
  • 应用——懒惰初始化单例模式
    • 懒惰实例化
    • 初始化时的线程安全有保障
public final class Singleton {
	private Singleton() { }
	// 内部类中保存单例
	private static class LazyHolder {
		static final Singleton INSTANCE = new Singleton();
	}
	// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
	public static Singleton getInstance() {
		return LazyHolder.INSTANCE;
	}
}

五、类加载器

以JDK8为例

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

1、启动类加载器

  • 用 Bootstrap 类加载器加载类:
    源代码:
public class F {
	static {
		System.out.println("bootstrap F init");
	}
}

执行

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

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

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

2、扩展类加载器

源代码

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

步骤

  • 将class G打成jar包
  • 将 jar 包拷贝到 JAVA_HOME/jre/lib/ext

执行

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

输出

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

3、双亲委派模式

  • 双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
  • Loadclass源码分析:
    • 检查该类是否已经加载
    • 如果有上级的话,委派上级loadclass
    • 如果没有上级的话(扩展加载器),委派启动类加载器加载
    • 如果还是没有找到 调用findClass方法(每个类加载器自己扩展)来加载

4、线程上下文类加载器

  • 线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中。
private S nextService() {
	if (!hasNextService())
		throw new NoSuchElementException();
	String cn = nextName;
	nextName = null;
	Class<?> c = null;
	try {
		c = Class.forName(cn, false, loader);
	} catch (ClassNotFoundException x) {
		fail(service,"Provider " + cn + " not found");
	}
	if (!service.isAssignableFrom(c)) {
		fail(service,"Provider " + cn + " not a subtype");
	}
	try {
		S p = service.cast(c.newInstance());
		providers.put(cn, p);
		return p;
	} catch (Throwable x) {
		fail(service,
			"Provider " + cn + " could not be instantiated",
			x);
	}
	throw new Error(); // This cannot happen
}
	

5、自定义类加载器

  • 适用情况
    • 想加载非 classpath 随意路径中的类文件
    • 都是通过接口来使用实现,希望解耦时,常用在框架设计
    • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
  • 步骤
    • 继承 ClassLoader 父类
    • 要遵从双亲委派机制,重写findClass 方法
      • 注意不是重写 loadClass 方法,否则不会走双亲委派机制
    • 读取类文件的字节码
    • 调用父类的 defineClass 方法来加载类
    • 使用者调用该类加载器的 loadClass 方法
  • 案例
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);
        }
    }
}

六、运行期优化

1、即时编译

  • 分层编译

    • JVM将执行状态分为五层
      • 0 层,解释执行(Interpreter)
      • 1 层,使用 C1 即时编译器编译执行(不带 profifiling)
      • 2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)
      • 3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)
      • 4 层,使用 C2 即时编译器编译执行
    • 注意
      • profifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
      • JIT(即时编译器)是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译。
      • 对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter(解释器) < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),并进行优化。
      • 刚才的一种优化手段称之为逃逸分析,发现新建的对象是否逃逸。可以使用 -XX:- DoEscapeAnalysis 关闭逃逸分析
  • 方法内联(inlining)

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

    • 案例

private static int square(final int i) {
	return i * i;
}
System.out.println(square(9));
  • 还能够进行常量折叠(constant folding)的优化
System.out.println(81);
  • 参数

    • -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印inlining 信息
    • -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
    • -XX:+PrintCompilation 打印编译信息
  • 字段优化

    • 成员变量和静态成员变量的优化,方法内联会影响这种优化,因此尽量使用局部变量
    • JMH 基准测试请参考

2、反射优化

  • arthas工具 java -jar arthas-boot.jar
    • 选择 进程前序号 回车表示分析该进程
    • 再输入【jad + 类名】来进行反编译
    • 通过查看 ReflectionFactory 源码可知
      • sun.reflflect.noInflflation 可以用来禁用膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
      • sun.reflflect.inflflationThreshold 可以修改膨胀阈值
  • 使用反射调用当次数超过膨胀阈值(value=15)时会自动生成新的方法直接调用,而不是反射调用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值