视频链接:黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓_哔哩哔哩_bilibili
跟着视频做的笔记
1 类文件结构
通过 javac 类名.java 编译 java 文件后,会生成一个 .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规范,类文件结构如下:
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.1 魔数
1.2 版本
51是jdk7,52是jdk8,53是jdk9
1.3 常量池
官方文档:Chapter 6. The Java Virtual Machine Instruction Set
2 字节码指令
2.1 两组字节码指令
2.1.1 构造方法的字节码指令
public cn.itcast.jvm.t5.HelloWorld();构造方法的字节码指令
2a b7 00 01 b1
-
2a => aload_0加载 slot 0的局部变量,即this,做为下面的invokespecial构造方法调用的参数
-
b7=> invokespecial 预备调用构造方法,哪个方法呢?
-
00 01引用常量池中#1项,即【Hethod java/lang/object." init>":()v】
-
b1表示返回
2.1.2 主方法的字节码指令
public static void main(java.lang.String[]);主方法的字节码指令
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.printIn:(Ljava/lang/String;)V】
-
bl表示返回
2.2 javap工具
Oracle 提供了 javap 工具来反编译 class 文件
D:\Idea\Code>cd jvm\out\production\jvm\cn\itcast\jvm\t5
D:\Idea\Code\jvm\out\production\jvm\cn\itcast\jvm\t5>javap -v HelloWorld
.class
Classfile /D:/Idea/Code/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWor
ld.class
Last modified 2022-4-18; size 567 bytes //文件最新修改时间;文件大小
MD5 checksum 8efebdac91aa496515fa1c161184e354 //MD5的签名
Compiled from "HelloWorld.java" //java源文件对应的是HelloWorld.java
public class cn.itcast.jvm.t5.HelloWorld //类的全路径名称
minor version: 0
major version: 52 //52代表的是jdk8
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V Methodref:引用方法
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/ Fieldref:调用对象
io/PrintStream;
#3 = String #23 // hello world String:字符串
#4 = Methodref #24.#25 // java/io/PrintStream.println
:(Ljava/lang/String;)V
#5 = Class #26 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;
)V
#26 = Utf8 cn/itcast/jvm/t5/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{//54-67:构造方法
public cn.itcast.jvm.t5.HelloWorld();
descriptor: ()V //方法参数信息
flags: ACC_PUBLIC //访问修饰符
Code: //具体代码
stack=1, locals=1, args_size=1 //stack=1:最大操作栈的深度 locals=1:局部变量表的长度 args_size=1:参数的长度
0: aload_0 //局部变量中的第0项加载到操作数栈
1: invokespecial #1 // Method java/lang/Object
."<init>":()V
4: return // 0 、1 、4 :字节码上的代码号
LineNumberTable: //code中的方法属性
line 4: 0 //line 4:java中源代码的行号 0:字节码上的行号
LocalVariableTable: //本地变量表
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
//start:起始范围 Slot:槽位号 Signature:局部变量的类型是HelloWorld类型
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.
out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStr invokevirtual:方法调用
eam.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
2.3 图解方法的执行流程
2.3.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);
}
}
2.3.2 编译后的字节码文件
D:\Idea\Code\jvm\out\production\jvm\cn\itcast\jvm>cd t3/bytecode
D:\Idea\Code\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode>javap -v D
emo3_1.class
Classfile /D:/Idea/Code/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode
/Demo3_1.class
Last modified 2022-4-18; 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/D
emo3_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/PrintStr
eam.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Demo3_1.java"
2.3.3 常量池载入运行时常量池
2.3.4 方法字节码载入方法区
2.3.5 main线程开始运行,分配栈帧内存
2.3.6 执行引擎开始执行字节码
bipush 10
-
bipush 10
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有 -
sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
-
ldc 将一个 int 压入操作数栈
-
ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
-
这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore 1
将操作数栈栈顶元素弹出,放入局部变量表的 slot 1 中 对应代码中的 a = 10
ldc #3
-
读取运行时常量池中 #3 ,即 32768 (超过 short 最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
-
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的。
istore 2
将操作数栈中的元素弹出,放到局部变量表的 2 号位置
iload 1
将局部变量表中 1 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作
iload 2
将局部变量表中2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作
iadd
将局部变量表中 1 号位置和 2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作
istore 3
将操作数栈中的元素弹出,放入局部变量表的3号位置。
getstatic #4
在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中
load 3
将局部变量表中 3 号位置的元素压入操作数栈中。
invokevirtual #5
-
找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
-
生成新的栈帧(分配 locals、stack等)
-
传递参数,执行新栈帧中的字节码
-
执行完毕,弹出栈帧
-
清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧,程序结束
3 编译器处理
所谓的 语法糖
,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
3.1 默认构造器
public class Candy1 {
}
编译成class后的代码
public class Candy1 {
// 这个无参构造器是java编译器帮我们加上的
public Candy1() {
// 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
super();
}
}
3.2 自动拆装箱
基本类型和其包装类型的相互转换过程,称为拆装箱 在 JDK 5 以后,它们的转换可以在编译期自动完成
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
转换过程如下
public class Candy2 {
public static void main(String[] args) {
// 基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
// 包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
3.3 泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer x = list.get(0);
}
}
字节码文件
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
// 这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
// 这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
// 这里进行了类型转换,将 Object 转换成了 Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
所以调用 get 函数取值时,有一个类型转换的操作。
Integer x = (Integer) list.get(0);
如果要将返回结果赋值给一个 int 类型的变量,则还有自动拆箱的操作
int x = (Integer) list.get(0).intValue();
使用反射可以得到,参数的类型以及泛型类型。泛型反射代码如下
public static void main(String[] args) throws NoSuchMethodException {
// 1. 拿到方法
Method method = Code_20_ReflectTest.class.getMethod("test", List.class, Map.class);
// 2. 得到泛型参数的类型信息
Type[] types = method.getGenericParameterTypes();
for(Type type : types) {
// 3. 判断参数类型是否,带泛型的类型。
if(type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
// 4. 得到原始类型
System.out.println("原始类型 - " + parameterizedType.getRawType());
// 5. 拿到泛型类型
Type[] arguments = parameterizedType.getActualTypeArguments();
for(int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
}
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}
输出:
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
3.4 可变参数
public class Candy4 {
public static void foo(String... args) {
// 将 args 赋值给 arr ,可以看出 String... 实际就是 String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同 样 java 编译器会在编译期间将上述代码变换为:
public class Candy4 {
public Candy4 {}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]);
}
}
注意,如果调用的是 foo() ,即未传递参数时,等价代码为 foo(new String[]{}) ,创建了一个空数组,而不是直接传递的 null .
3.5 foreach循环
仍是 JDK 5 开始引入的语法糖,数组的循环
public class Candy5 {
public static void main(String[] args) {
// 数组赋初值的简化写法也是一种语法糖。
int[] arr = {1, 2, 3, 4, 5};
for(int x : arr) {
System.out.println(x);
}
}
}
编译器会帮我们转换为
public class Candy5 {
public Candy5() {}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
如果是集合使用 foreach
public class Candy5 {
public Candy5(){}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 获得该集合的迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer x = iterator.next();
System.out.println(x);
}
}
}
3.6 switch-String
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Cnady6_1 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
在编译器中执行的操作的过程说明:
-
在编译期间,单个的 switch 被分为了两个
-
第一个用来匹配字符串,并给 x 赋值
-
字符串的匹配用到了字符串的 hashCode ,还用到了 equals 方法
-
使用 hashCode 是为了提高比较效率,使用 equals 是防止有 hashCode 冲突(如 "BM" 和 "C." ,他们的hashCode值都为2123,例如Demo6_2)
-
第二个用来根据x的值来决定输出语句
public class Candy6_1 {
public Candy6_1() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
// 通过字符串的 hashCode + value 来判断是否匹配
switch (str.hashCode()) {
// hello 的 hashCode
case 99162322 :
// 再次比较,因为字符串的 hashCode 有可能相等
if(str.equals("hello")) {
x = 0;
}
break;
// world 的 hashCode
case 11331880 :
if(str.equals("world")) {
x = 1;
}
break;
default:
break;
}
// 用第二个 switch 在进行输出判断
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
break;
default:
break;
}
}
}
package cn.itcast.jvm.t3.candy;
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;
}
}
}
}
3.7 switch-enum
enum SEX {
MALE, FEMALE;
}
public class Candy7 {
public static void main(String[] args) {
SEX sex = SEX.MALE;
switch (sex) {
case MALE:
System.out.println("man");
break;
case FEMALE:
System.out.println("woman");
break;
default:
break;
}
}
}
编译器中执行的代码如下
enum SEX {
MALE, FEMALE;
}
public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存放了 case 用于比较的数字
static int[] map = new int[2];
static {
// ordinal 即枚举元素对应所在的位置,MALE 为 0 ,FEMALE 为 1
map[SEX.MALE.ordinal()] = 1;
map[SEX.FEMALE.ordinal()] = 2;
}
}
public static void main(String[] args) {
SEX sex = SEX.MALE;
// 将对应位置枚举元素的值赋给 x ,用于 case 操作
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("man");
break;
case 2:
System.out.println("woman");
break;
default:
break;
}
}
}
3.8 枚举类
JDK 7 新增了枚举类,以前面的性别枚举为例:
enum SEX {
MALE, FEMALE;
}
转换后的代码
public final class Sex extends Enum<Sex> {
// 对应枚举类中的元素
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
// 调用构造函数,传入枚举元素的值及 ordinal
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
// 调用父类中的方法
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
3.9 try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法,‘try-with-resources’,使得我们在关闭资源时不需要写finally
try(资源变量 = 创建资源对象) {
} catch() {
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-with- resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:
package cn.itcast.jvm.t3.candy;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
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 异常");
}
}
输出
java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)
3.10 方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
-
父子类的返回值完全一致
-
子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
对于子类,java 编译器会做如下处理
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以 用下面反射代码来验证:
public static void main(String[] args) {
for(Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
}
结果
public java.lang.Integer cn.ali.jvm.test.B.m()
public java.lang.Number cn.ali.jvm.test.B.m()
3.11 匿名内部类
public class Candy10 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("running...");
}
};
}
}
转换后的代码
public class Candy10 {
public static void main(String[] args) {
// 用额外创建的类来创建匿名内部类对象
Runnable runnable = new Candy10$1();
}
}
// 创建了一个额外的类,实现了 Runnable 接口
final class Candy10$1 implements Runnable {
public Demo8$1() {}
@Override
public void run() {
System.out.println("running...");
}
}
引用局部变量的匿名内部类,源代码:
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 对象的 值后,如果不是 final 声明的 x 值发生了改变,匿名内部类则值不一致。
4 类加载阶段
4.1 加载
-
将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
-
_java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
-
_super 即父类
-
_fields 即成员变量
-
_methods 即方法
-
_constants 即常量池
-
_class_loader 即类加载器
-
_vtable 虚方法表
-
_itable 接口方法
-
-
如果这个类还有父类没有加载,先加载父类
-
加载和链接可能是交替运行的
注意
-
instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
-
_java_mirror则是保存在堆内存中
-
InstanceKlass和.class(JAVA镜像类)互相保存了对方的地址
-
类的对象在对象头中保存了.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
-
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
-
可以通过前面介绍的 HSDB 工具查看
4.2 链接
验证:验证类是否符合 JVM规范,安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
准备:为 static 变量分配空间,设置默认值
解析:将常量池中的符号引用解析为直接引用
-
-
符号引用:仅仅只是符号,不知道类、方法、属性在内存中的位置
-
直接引用:确切知道类、方法、属性在内存中的位置
-
使用Hotspot虚拟机查看
-
在idea的Terminal中查看
-
jps:查看进程id
-
切换到jdk目录下:
-
D:\>cd C:/Program Files/Java/jdk1.8.0_241 D:\>c:
-
-
打开Hotspot
-
C:\Program Files\Java\jdk1.8.0_241>java -cp ./lib/sa-jdi.jar sun.jvm.hot spot.HSDB
-
-
-
package cn.itcast.jvm.t3.load; import java.io.IOException; /** * 解析的含义 */ 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(); System.in.read(); } } class C { D d = new D(); } class D { }
-
4.3 初始化
<cinit>()v 方法
初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全
导致类初始化的情况
概括得说,类初始化是【懒惰的】
-
main 方法所在的类,总会被首先初始化
-
首次访问这个类的静态变量或静态方法时
-
子类初始化,如果父类还没初始化,会引发
-
子类访问父类的静态变量,只会触发父类的初始化
-
Class.forName
-
new 会导致初始化
不会导致类初始化的情况
-
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
-
类对象.class 不会触发初始化
-
创建该类的数组不会触发初始化
-
类加载器的loadClass方法
-
Class.forName的参数2为flase时
代码示例
package cn.itcast.jvm.t3.load;
import java.io.IOException;
public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException, IOException {
// // 1. 静态常量不会触发初始化
// System.out.println(B.b);
// // 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// // 3. 创建该类的数组不会触发初始化
// System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("cn.itcast.jvm.t3.load.B");
// // 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
System.in.read();
// // 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);
// // 2. 子类初始化,如果父类还没初始化,会引发
// System.out.println(B.c);
// // 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);
// // 4. 会初始化类 B,并先初始化类 A
// Class.forName("cn.itcast.jvm.t3.load.B");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
4.4 练习
1. 从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
public class Load2 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
// 会导致 E 类初始化,因为 Integer 是包装类
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;// Integer.value(20)
static {
System.out.println("E cinit");
}
}
2. 典型应用 - 编写完成懒惰初始化单例模式
package cn.itcast.jvm.t3.load;
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;
}
}
5 类加载器
以jdk8为例:级别从高到低
5.1 启动类加载器
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()); // AppClassLoader ExtClassLoader
}
}
5.2 拓展类加载器
如果 classpath 和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载。
package cn.itcast.jvm.t3.load;
public class G {
static {
// System.out.println("ext G init");
System.out.println("classpath G init");
}
}
执行
package cn.itcast.jvm.t3.load;
/**
* 演示 扩展类加载器
* 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
* 里面也有一个 G 的类,观察到底是哪个类被加载了
*/
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
结果
"C:\Program Files\Java\jdk1.8.0_241\bin\java.exe" "-javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=59987:D:\Idea\IntelliJ IDEA 2019.2.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_241\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\rt.jar;D:\Idea\Code\jvm\out\production\jvm;D:\Idea\Code\jvm\lib\jackson-core-2.3.3.jar;D:\Idea\Code\jvm\lib\jackson-databind-2.3.3.jar;D:\Idea\Code\jvm\lib\jackson-annotations-2.3.3.jar" cn.itcast.jvm.t3.load.Load5_2
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
5.3 双亲委派模式
双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则。 loadClass源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先查找该类是否已经被该类加载器加载过了
Class<?> c = findLoadedClass(name);
// 如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 看是否被它的上级加载器加载过了 Extension 的上级是Bootstarp,但它显示为null
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 看是否被启动类加载器加载过
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//捕获异常,但不做任何处理
}
if (c == null) {
// 如果还是没有找到,先让拓展类加载器调用 findClass 方法去找到该类,如果还是没找到,就抛出异常
// 然后让应用类加载器去找 classpath 下找该类
long t1 = System.nanoTime();
c = findClass(name);
// 记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
5.4 线程上下文类加载器
线程上下文类加载器是比较特殊的加载器。我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写 Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?让我们追踪一下源码:
class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
System.out.println("JDBC DriverManager initialized");
}
}
先不看别的,看看 DriverManager 的类加载器:
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib/ 下显然没有 mysql-connect-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
继续看 loadInitialDrivers() 方法:JDK 在某些情况下需要打破双亲委派模式
private static void loadInitialDriver() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
@Override
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1) 使用ServiceLoader 机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
ServiceLoader<Driver> loaderDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loaderDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
System.out.println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2) 使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
System.out.println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
System.out.println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemLoader() 就是应用程序类加载器
// getSystemClassLoader 其实就是应用程序类加载器,打破了双亲委派模式。在某些情况下需要打破双亲委派模式
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
System.out.println("DriverManager.Initialize: load failed: " + ex);
}
}
}
再看如下,它就是大名鼎鼎 Service Provider Interface (SPI),主要是为了解耦
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称。即根据接口找到文件,文件内容是要加载类的类名
这样就可以使用
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while (iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程 + 解耦】的思想,在下面一些框架中都运用了此思想:
-
JDBC
-
Servlet 初始化器
-
Spring 容器
-
Dubbo (对 SPI 进行了扩展)
接着看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, c1);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 java.util.ServiceLoader 的内部类 LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
5.5 自定义类加载器
问问自己,什么时候需要自定义类加载器
-
想加载非 classpath 随意路径的类文件
-
都是通过接口使用实现,希望解耦时,常用在框架设计
-
这些希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
-
继承 ClassLoader 父类
-
要遵从双亲委派机制,重写 findClass() 方法
-
注意:不是重写 loadClass() 方法,否则不会走双亲委派机制
-
-
读取类文件的字节码
-
调用父类的 defineClass() 方法来加载类
-
使用者调用该类加载器的 loadClass() 方法
案例
package cn.itcast.jvm.t3.load;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
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 运行期优化
-
Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文中简称JIT编译器)。
-
由于Java虚拟机规范没有具体的约束规则去限制即时编译器应该如何实现,所以这部分功能完全是与虚拟机具体实现(Implementation Specific)相关的内容,如无特殊说明,本文提及的编译器、即时编译器都是指HotSpot虚拟机内的即时编译器,虚拟机也是特指HotSpot虚拟机。
-
解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
6.1 即时编译
6.1.1 分层编译(逃逸分析)
package cn.itcast.jvm.t3.jit;
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));
}
}
}
执行时间片段如下:
0 86861
1 89557
2 68333
3 64021
4 67292
5 63446
-------------
67 22369
68 44501
69 30545
70 17963
71 21750
-------------
195 13104
196 14453
197 23107
198 13574
199 14051
测试结果:我们可以看到同样的代码,为什么后续执行的时间越来越短呢?原因是什么呢?这就需要先了解 JVM 即时编译知识。
由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能更长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启用分层编译
(Tiered Compilation)的策略。
JVM 将执行状态分成了 5 个层次:
-
0层,解释执行(Interpreter),将字节码解释为机器码
-
1层,使用 C1 即时编译器编译执行(不带 profiling)
-
profiling
是指在运行过程中收集一些程序执行状态的数据,即信息统计工作,例如【方法的调用次数】,【循环的回边次数】等。
-
-
2层,使用 C1 即时编译器编译执行(带基本的 profiling)
-
3层,使用 C1 即时编译器编译执行(带完全的 profiling)
-
4层,使用 C2 即时编译器编译执行
即时编译器(JIT)与解释器的区别
-
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
-
JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,下次遇到相同的代码,直接执行,无需再编译
-
解释器是将字节码解释为针对所有平台都通用的机器码
-
JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下 :Interpreter < C1(可以提升5倍左右) < C2(可以提升10~100倍),总的目标是发现热点代码 (hotspot名称的由来)优化之。
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭,默认打开,再运行刚才的示例观察结果。会才发现后续的运行时间没有大缩短了。
6.1.2 方法内联
private static int square(final int i) {
return i * i;
}
方法内联:如果发现 square() 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置
System.out.println(square(9));
还能够进行常量折叠(constant folding)的优化
System.out.println(81);
测试代码如下
package cn.itcast.jvm.t3.jit;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public class JIT2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:CompileCommand=dontinline,*JIT2.square
// -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();
// 输出计算i*i所花费的时间
System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
}
}
private static int square(final int i) {
return i * i;
}
}
测试结果如下
1 81 79805
2 81 35501
3 81 35220
4 81 31679
5 81 35823
--------------------------
91 81 7316
92 81 7322
93 81 7327
94 81 8118
95 81 7441
--------------------------
131 81 61
132 81 54
133 81 53
134 81 54
135 81 52
如果在上述代码加入 VM 参数(查看内联方法):-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining,再次运行
如果我们禁用内联:-XX:CompileCommand=dontinline,*T02_RunTime_Inlining.square,再次测试,耗时片断如下:
1 81 49707
2 81 31806
3 81 22157
4 81 35130
5 81 35269
----------------------
91 81 8425
92 81 8722
93 81 13402
94 81 8650
95 81 8596
----------------------
131 81 5365
132 81 5444
133 81 5633
134 81 4731
135 81 5293
相比第一次测试,到了130多次后,耗时再没有下降,因为内联关闭了
6.1.3 字段优化
导入依赖
<dependencies>
<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>
</dependencies>
编写测试代码
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)
//5轮测试
@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.DONT_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();
}
}
测试
ops/s:每秒内能调用的吞吐量。每秒吞吐量,分数越高的更好)
开启方法内联
每个方法2轮热身,5轮测试
@CompilerControl(CompilerControl.Mode.INLINE) static void doSum(int x) { sum += x; }
测试结果如下:
"C:\Program Files\Java\jdk1.8.0_241\bin\java.exe" "-javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53522:D:\Idea\IntelliJ IDEA 2019.2.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_241\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\rt.jar;D:\Idea\Code\jvm\jmh\target\classes;C:\Users\h'p\.m2\repository\org\openjdk\jmh\jmh-core\1.0\jmh-core-1.0.jar;C:\Users\h'p\.m2\repository\net\sf\jopt-simple\jopt-simple\4.6\jopt-simple-4.6.jar;C:\Users\h'p\.m2\repository\org\apache\commons\commons-math3\3.2\commons-math3-3.2.jar" test.Benchmark1
// test1()方法开始
# VM invoker: C:\Program Files\Java\jdk1.8.0_241\jre\bin\java.exe
# VM options: -javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53522:D:\Idea\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: test.Benchmark1.test1
# Run progress: 0.00% complete, ETA 00:00:21
# Fork: 1 of 1
# Warmup Iteration 1: 3775229.009 ops/s
# Warmup Iteration 2: 3801189.225 ops/s
Iteration 1: 3836100.599 ops/s
Iteration 2: 3773069.329 ops/s
Iteration 3: 3801847.269 ops/s
Iteration 4: 3805983.829 ops/s
Iteration 5: 3735229.542 ops/s
Result: 3790446.113 ±(99.9%) 146711.067 ops/s [Average]
Statistics: (min, avg, max) = (3735229.542, 3790446.113, 3836100.599), stdev = 38100.398
Confidence interval (99.9%): [3643735.046, 3937157.180]
// test2()方法开始
# VM invoker: C:\Program Files\Java\jdk1.8.0_241\jre\bin\java.exe
# VM options: -javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53522:D:\Idea\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: test.Benchmark1.test2
# Run progress: 33.33% complete, ETA 00:00:19
# Fork: 1 of 1
# Warmup Iteration 1: 3823323.546 ops/s
# Warmup Iteration 2: 3830211.517 ops/s
Iteration 1: 3818401.135 ops/s
Iteration 2: 3830033.123 ops/s
Iteration 3: 3786045.263 ops/s
Iteration 4: 3775771.737 ops/s
Iteration 5: 3797301.114 ops/s
Result: 3801510.474 ±(99.9%) 86494.112 ops/s [Average]
Statistics: (min, avg, max) = (3775771.737, 3801510.474, 3830033.123), stdev = 22462.246
Confidence interval (99.9%): [3715016.363, 3888004.586]
// test3()方法开始
# VM invoker: C:\Program Files\Java\jdk1.8.0_241\jre\bin\java.exe
# VM options: -javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53522:D:\Idea\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: test.Benchmark1.test3
# Run progress: 66.67% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration 1: 3814675.094 ops/s
# Warmup Iteration 2: 3794319.015 ops/s
Iteration 1: 3858357.490 ops/s
Iteration 2: 3783819.772 ops/s
Iteration 3: 3833271.273 ops/s
Iteration 4: 3797008.696 ops/s
Iteration 5: 3788820.274 ops/s
Result: 3812255.501 ±(99.9%) 124139.553 ops/s [Average]
Statistics: (min, avg, max) = (3783819.772, 3812255.501, 3858357.490), stdev = 32238.648
Confidence interval (99.9%): [3688115.948, 3936395.054]
// 3种循环方法比较其每秒的吞吐量
# Run complete. Total time: 00:00:28
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 3790446.113 146711.067 ops/s
t.Benchmark1.test2 thrpt 5 3801510.474 86494.112 ops/s
t.Benchmark1.test3 thrpt 5 3812255.501 124139.553 ops/s
Process finished with exit code 0
关闭方法内联
每个方法2轮热身,5轮测试
@CompilerControl(CompilerControl.Mode.DONT_INLINE) static void doSum(int x) { sum += x; }
测试结果如下:
"C:\Program Files\Java\jdk1.8.0_241\bin\java.exe" "-javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53324:D:\Idea\IntelliJ IDEA 2019.2.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_241\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_241\jre\lib\rt.jar;D:\Idea\Code\jvm\jmh\target\classes;C:\Users\h'p\.m2\repository\org\openjdk\jmh\jmh-core\1.0\jmh-core-1.0.jar;C:\Users\h'p\.m2\repository\net\sf\jopt-simple\jopt-simple\4.6\jopt-simple-4.6.jar;C:\Users\h'p\.m2\repository\org\apache\commons\commons-math3\3.2\commons-math3-3.2.jar" test.Benchmark1
// test1()方法开始
# VM invoker: C:\Program Files\Java\jdk1.8.0_241\jre\bin\java.exe
# VM options: -javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53324:D:\Idea\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: test.Benchmark1.test1
# Run progress: 0.00% complete, ETA 00:00:21
# Fork: 1 of 1
# Warmup Iteration 1: 407572.050 ops/s
# Warmup Iteration 2: 456499.779 ops/s
Iteration 1: 455916.396 ops/s
Iteration 2: 469892.965 ops/s
Iteration 3: 458752.739 ops/s
Iteration 4: 459871.281 ops/s
Iteration 5: 444705.953 ops/s
Result: 457827.867 ±(99.9%) 34785.824 ops/s [Average]
Statistics: (min, avg, max) = (444705.953, 457827.867, 469892.965), stdev = 9033.768
Confidence interval (99.9%): [423042.043, 492613.691]
// test2()方法开始
# VM invoker: C:\Program Files\Java\jdk1.8.0_241\jre\bin\java.exe
# VM options: -javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53324:D:\Idea\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: test.Benchmark1.test2
# Run progress: 33.33% complete, ETA 00:00:19
# Fork: 1 of 1
# Warmup Iteration 1: 568777.479 ops/s
# Warmup Iteration 2: 561910.004 ops/s
Iteration 1: 574847.643 ops/s
Iteration 2: 573289.807 ops/s
Iteration 3: 562460.971 ops/s
Iteration 4: 571991.193 ops/s
Iteration 5: 563014.548 ops/s
Result: 569120.833 ±(99.9%) 22785.284 ops/s [Average]
Statistics: (min, avg, max) = (562460.971, 569120.833, 574847.643), stdev = 5917.266
Confidence interval (99.9%): [546335.548, 591906.117]
// test3()方法开始
# VM invoker: C:\Program Files\Java\jdk1.8.0_241\jre\bin\java.exe
# VM options: -javaagent:D:\Idea\IntelliJ IDEA 2019.2.4\lib\idea_rt.jar=53324:D:\Idea\IntelliJ IDEA 2019.2.4\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: test.Benchmark1.test3
# Run progress: 66.67% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration 1: 571154.246 ops/s
# Warmup Iteration 2: 573143.694 ops/s
Iteration 1: 571557.893 ops/s
Iteration 2: 557767.992 ops/s
Iteration 3: 583281.049 ops/s
Iteration 4: 580190.054 ops/s
Iteration 5: 579166.244 ops/s
Result: 574392.647 ±(99.9%) 39448.707 ops/s [Average]
Statistics: (min, avg, max) = (557767.992, 574392.647, 583281.049), stdev = 10244.704
Confidence interval (99.9%): [534943.940, 613841.353]
// 3种循环方法比较其每秒的吞吐量
# Run complete. Total time: 00:00:28
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 457827.867 34785.824 ops/s
t.Benchmark1.test2 thrpt 5 569120.833 22785.284 ops/s
t.Benchmark1.test3 thrpt 5 574392.647 39448.707 ops/s
Process finished with exit code 0
测试结果分析
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
// doSum(elements[i]);
doSum(elements[i]); // 1000 次取下标 i 的元素 <- local
}
}
可以节省 1999 次 Field 字段读取操作
但如果 doSum() 方法没有内联,则不会进行上面的优化
本地变量访问长度、数据时,不需要去 class 元数据那里找,在本地变量就可以找到了,相当于手动优化。但是方法内联是由虚拟机来优化的。所以,test3 方法与test2 方法是等价的,test1 方法是运行期间优化了,test2 方法是手动优化了, test3 方法的 foreach 是 编译期间优化了。
6.2 反射优化
通过“反射”我们可以动态的获取到对象的信息以及灵活的调用对象方法等,但是在使用的同时又伴随着另一种声音的出现,那就是“反射”很慢,要少用。那么 JVM 是怎样做了反射优化的呢?下面我们一起来分析一下,示例代码:
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 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();
}
}
当执行第16次时,速度会加快
查看 MethodAccessor 的实现类,进一步查看反射优化,会发现有个15次的阈值
注意:
通过查看 ReflectionFactory 源码可知
-
sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
-
sun.reflect.inflationThreshold 可以修改膨胀阈值