本篇博客是记录个人学习JVM的笔记,学习的视频是哔哩哔哩中 黑马的jvm教程;
视频链接:jvm学习
上一篇文章:JVM 垃圾回收 超详细学习笔记(二)未来很长,别只看眼前的博客-CSDN博客
目录
先来大概看看jvm的结构:

根据 JVM 规范,类文件结构如下:这些东西先了解就行,主要是会观看javap -v 之后的字节码就行,这个二进制的不做要求;
- u4 magic //u4表示的4个字节 魔数
- u2 minor_version; //u2表示的是两个字节 版本
- 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];
编译下面的代码得到class文件,然后使用一些可以读取二进制文件的工具打开,就可以看到二进制的内容了:
- public class HelloWorld {
- public static void main(String[] args) {
- System.out.println("hello world");
- }
- }
- 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
u4 magic 表示的是前面四个字节(当然有时候并不一定是4个字节)表示的是魔数,下面加粗的部分; 对应字节码文件的 0~3 个字节; 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 ca fe ba be :意思是 .class 文件,不同的东西有不同的魔数,比如 jpg、png 图片等!
魔数的作用是表示文件的类型;
4~7字节,表示类的版本,00 34(52)表示的是Java8
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 15(8进制的15翻译成10进制为21)表示它引用了常量池中#6和#21项来获得这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
等等......
有一个专门的表格,对着表格去看。。。。。。。
可以使用idea的一个插件来帮助阅读:

安装完成后:


自己看这些二进制的文件一个一个对着表去找,有点太麻烦了,所以Oracle就提供了Javap工具来反编译class文件,反编译后的文件可阅读性更加强;
javap -v HelloWorld.class (对class文件进行反编译)

- Classfile /E:/jvm_learn/out/production/jvm_learn/classLoad/HelloWorld.class
- Last modified 2022-6-23; size 553 bytes //表示文件最后修改时间 以及文件大小
- MD5 checksum afcde712c515c318c66483094430ce48 //md5的矫正签名
- Compiled from "HelloWorld.java" //编译后的源文件
- public class classLoad.HelloWorld //类的全路径名称
- minor version: 0
- major version: 52 //代表jdk8
- flags: ACC_PUBLIC, ACC_SUPER //类的访问修饰符
- Constant pool: //常量池,下面的#都是引用关系和给jvm查找使用的
- #1 = Methodref #6.#20 //这个给的注释,是通过查询常量池表后最后的查询结果就直接告诉我们来,不需要我们再对着这个常量池一个一个去查询了 java/lang/Object."<init>":()V
- #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
- #3 = String #23 // hello world
- #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V string类型的参数 返回的是void
- #5 = Class #26 // classLoad/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 LclassLoad/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 classLoad/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
- { //方法信息
- public classLoad.HelloWorld(); //构造方法
- descriptor: ()V //方法的参数信息 v 表示void
- flags: ACC_PUBLIC //访问修饰符
- Code: //代码
- stack=1, locals=1, args_size=1 //分别是 栈道最大操作深度 局部变量表的长度 参数的长度
- 0: aload_0 //把局部变量的第0项加载到操作数栈
- 1: invokespecial #1//调用方法,常量池中的第一项 // Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable: //方法中的属性,对应着一个行号表
- line 7: 0 //前面的行号7代表的是源代码中的行号,后面的行号0代表的是字节码中的行号
- LocalVariableTable: //对应的是本地变量表
- Start Length Slot Name Signature
- 0 5 0 this LclassLoad/HelloWorld;
- //start表示的是字节码的起始范围,length表示的是【作用的范围】 slot表示的是槽位号 name表示的是变量名 Signature表示的是局部变量的类型
- public static void main(java.lang.String[]); //main 方法
- descriptor: ([Ljava/lang/String;)V //方法的参数是string
- flags: ACC_PUBLIC, ACC_STATIC //访问修饰符是public static
- Code:
- stack=2, locals=1, args_size=1
- 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
- 3: ldc #3 //ldc表示的是加载常量 // String hello world
- 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 8: return
- LineNumberTable: //源代码的行号和字节码中的行号
- line 10: 0
- line 11: 8
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 9 0 args [Ljava/lang/String;
- }
- SourceFile: "HelloWorld.java"
- public class Demo {
- public static void main(String[] args) {
- int a = 10;
- int b = Short.MAX_VALUE + 1;
- int c = a + b;
- System.out.println(c);
- }
- }
编译后的字节码文件:放在这里仅供参考对照,下面将会使用图解的形式来解释这段代码的字节码指令;
- Classfile /E:/jvm_learn/out/production/jvm_learn/classLoad/Demo.class
- Last modified 2022-6-23; size 594 bytes
- MD5 checksum 36ce1276d35f9fadf2c98315263b059b
- Compiled from "Demo.java"
- public class classLoad.Demo
- 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 // classLoad/Demo
- #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 LclassLoad/Demo;
- #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 Demo.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 classLoad/Demo
- #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 classLoad.Demo();
- 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 7: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 5 0 this LclassLoad/Demo;
-
- 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 9: 0
- line 10: 3
- line 11: 6
- line 12: 10
- line 13: 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: "Demo.java"
图解:这样一段Java代码执行的时候回发生什么?
会由Java虚拟机中的类加载器来帮我们加载,把这些字节码加载到内存;而字节码文件中的常量池会被加载到运行时常量池;(这个运行时常量池属于方法区的一个部分)

32767 :是short的最大取值;
Java源码中一些较小的数值并不是存储在常量池的而是跟着这个方法的字节码指令存放在一起,一旦这个数值的大小超过了这个short的最大值,那么它就会存储在常量池中
从上面给的参考字节码也可以看出:
- 0: bipush 10 //a为10
- line 9: 0 //9为源代码中的第九行
- #3 = Integer 32768

然后main线程开始运行,分配栈帧内存;
(stack=2,locals=4) 对应操作数栈的深度是2,局部变量表的长度为4

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

istore 1:将操作数栈顶数据弹出,存入局部变量表的 slot 1
对应代码中的:a = 10


ldc #3:
读取运行时常量池中#3,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的;

istore 2: 将操作数【栈中的元素弹出】,放到局部变量表的2号位置


iload1 iload2:运算操作是要栈中完成的,在局部变量表中不能完成运算;
将局部变量表中1号位置和2号位置的元素读取出来放到操作数栈中;

iadd:将操作数栈中的两个元素弹出栈并相加,并加运算结果在压入操作数栈中


istore 3:将操作数栈中的元素弹出,放入局部变量表的3号位置;


getstatic #4:在运行时常量池中找到#4,发现是一个对象;在堆内存中找到该对象,并将其引用放入操作数栈中;


iload 3:将局部变量表中3号位置的元素压入操作数栈中

invokevirtual 5:
找到常量池 #5 项
定位到方法区 java/io/PrintStream.println:(I)V 方法
发现是方法后,由虚拟机分配一个新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码
执行完毕,弹出栈帧
清除 main 操作【数栈】内容;

return:完成 main 方法调用,弹出 main 栈帧,程序结束
参考本人的另一篇博客:
注意:while和for循环的字节码,你是发现它们是一模一样的,殊途也能同归;
- public class Test {
- public static void main(String[] args) {
- int i = 0;
- int x = 0;
- while (i < 10) {
- x = x++; //i++ 先执行ilaod_x 把局部变量中的x加载到操作数栈中,此时操作数栈中的x为0,然后执行iinc_x 1 ,把局部变量表中的x自增1,所以局部变量表中的x为1,然后又做了一个赋值操作把操作数栈中的x给赋值给局部变量表中的x,所以局部变量表中的x又变为0 ,循环10次后x依旧为0
- i++;
- }
- System.out.println(x); // 0
- }
- }
对应的字节码文件:
- Code:
- stack=2, locals=3, args_size=1 // 操作数栈分配2个空间,局部变量表分配 3 个空间
- 0: iconst_0 // 准备一个常数 0
- 1: istore_1 // 将常数 0 放入局部变量表的 1 号槽位 i = 0
- 2: iconst_0 // 准备一个常数 0
- 3: istore_2 // 将常数 0 放入局部变量的 2 号槽位 x = 0
- 4: iload_1 // 将局部变量表 1 号槽位的数放入操作数栈中
- 5: bipush 10 // 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
- 7: if_icmpge 21 // 比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到 21 。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
- 10: iload_2 // 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0
- 11: iinc 2, 1 // 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值为 1
- 14: istore_2 //将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了0
- 15: iinc 1, 1 // 1 号槽位的值自增 1
- 18: goto 4 // 跳转到第4条指令
- 21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
- 24: iload_2
- 25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
- 28: return
先看一道题:
- public class CinitTest {
- static int i = 10;
- static {
- i = 20;
- }
- static {
- i = 30;
- }
- public static void main(String[] args) {
- System.out.println(i); // 30
- }
- }
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :(这个方法是没有返回值也没有参数), 这个方法会在类的初始化阶段就会被调用;这个cinit()v是整个类的构造方法;
- stack=1, locals=0, args_size=0
- 0: bipush 10
- 2: putstatic #3 // Field i:I
- 5: bipush 20
- 7: putstatic #3 // Field i:I
- 10: bipush 30
- 12: putstatic #3 // Field i:I
- 15: return
这个init()V 构造方法是每个类的构造方法;
先来看一道题:
- public class InitTest {
-
- private String a = "s1";
-
- {
- b = 20;
- }
-
- private int b = 10;
-
- {
- a = "s2";
- }
-
- public InitTest(String a, int b) {
- this.a = a;
- this.b = b;
- }
- public static void main(String[] args) {
- InitTest d = new InitTest("s3", 30);
- System.out.println(d.a); //s3
- System.out.println(d.b); //30
- }
- }
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法(比如上面这个题中的InitTest(String a, int b)构造方法就是原始构造方法)内的代码也会附加到这个新的构造方法中,附加到最后;
下面是代码对应的字节码:
- Code:
- stack=2, locals=3, args_size=3
- 0: aload_0 //把this加载
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: aload_0
- 5: ldc #2 // String s1 把s1加载到数栈
- 7: putfield #3 // Field a:Ljava/lang/String; 相当于把s3赋值给this.a
- 10: aload_0
- 11: bipush 20
- 13: putfield #4 // Field b:I 把20赋值给this.b
- 16: aload_0
- 17: bipush 10
- 19: putfield #4 // Field b:I 把10赋值给this.b
- 22: aload_0
- 23: ldc #5 // String s2
- 25: putfield #3 // Field a:Ljava/lang/String;
- // 原始构造方法在最后执行 ---------下面是原始构造方法中的代码对应的字节码
- 28: aload_0 //加载this
- 29: aload_1 //加载局部变量slot 1(a) 对应着”s3"
- 30: putfield #3 // Field a:Ljava/lang/String;
- 33: aload_0
- 34: iload_2 //加载局部变量slot 2(b) 对应着30 在局部变量表中找到的,这里没给出来
- 35: putfield #4 // Field b:I
- 38: return
看一下几种不同方法调用对应的字节码指令:
- public class Demo5 {
- public Demo5() {
-
- }
-
- private void test1() {
-
- }
-
- private final void test2() {
-
- }
-
- public void test3() {
-
- }
-
- public static void test4() {
-
- }
-
- public static void main(String[] args) {
- Demo5 demo5 = new Demo5();
- demo5.test1(); //私有
- demo5.test2(); //final
- demo5.test3(); //公共
- Demo5.test4(); //静态
- }
- }
不同方法在调用时,对应的虚拟机指令有所区别
私有、构造、被final修饰的方法,在调用时都使用invokespecial指令
普通成员方法在调用时,使用invokespecial指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
静态方法在调用时使用invokestatic指令
注意:这里的invokespecial和invokestatic都是静态绑定,在字节码阶段就可以知道调用的是哪一个类的哪个方法,这个invokevirtual是公有的方法可能会出现重写,所以这个带public 的方法在字节码阶段是不知道这个方法具体是哪个类的,属于动态绑定;
- code:
- stack=2, locals=2, args_size=1
- 0: new #2 // class classLoad/Demo5 这个new分为两个步骤:调用构造方法首先会在堆中分配一块空间,然后空间分配成功后会把这个对象的引用放入操作数栈
- 3: dup //把刚刚创建的对象的引用地址复制一份,放到栈顶 ;为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”:()V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
- 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 final修饰的方法
- 16: aload_1
- 17: invokevirtual #6 // Method test3:()V public修饰的方法
- 20: invokestatic #7 // Method test4:()V 静态方法,因为静态方法不会被对象调用,所以静态方法一调用后就直接出栈了,所以平常也不要使用对象来调用静态方法了,不然会多产生一些不必要的指令
- 23: return
注意:new这个过程实际上是包括两步:开辟空间,空间分配成功后会把这个对象的引用放入操作数栈 (这一份是拷贝的对象的地址值);
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令
在执行invokevirtual指令时,经历了以下几个步骤
先通过栈帧中对象的引用找到对象
分析对象头,找到对象实际的Class
Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
查询vtable找到方法的具体地址
执行方法的字节码
- public class TryCatchTest {
-
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- }catch (Exception e) {
- i = 20;
- }
- }
- }
对应字节码指令:
- Code:
- stack=1, locals=3, args_size=1
- 0: iconst_0
- 1: istore_1
- 2: bipush 10
- 4: istore_1
- 5: goto 12
- 8: astore_2
- 9: bipush 20
- 11: istore_1
- 12: return
- //多出来一个异常表
- Exception table:
- from to target type
- 2 5 8 Class java/lang/Exception
可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
第8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号槽位(为 e )
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- }catch (ArithmeticException e) {
- i = 30;
- }catch () {
- i = 40;
- }catch(Exception e){
- i = 50;
- }
- }
- Code:
- stack=1, locals=3, args_size=1
- 0: iconst_0
- 1: istore_1
- 2: bipush 10
- 4: istore_1
- 5: goto 26 //发生异常跳到26行
- 8: astore_2 //存储异常对象的引用
- 9: bipush 30
- 11: istore_1
- 12: goto 26 //发生异常跳到26行
- 15: astore_2 //存储异常对象的引用
- 16: bipush 40
- 18: istore_1
- 19: goto 26 //发生异常跳到26行
- 22: astore_2 //存储异常对象的引用
- 23: bipush 50
- 25: istore_1
- 26: return
- Exception table:
- from to target type //这里的target和上面的行号对应
- 2 5 8 Class java/lang/ArithmeticException
- 2 5 15 Class java/lang/NullPointerException
- 2 5 22 Class java/lang/Exception
- LineNumberTable....
- LocalVariableTable: //Slot 这么多个2,是为了复用,因为这些异常同一时刻只能发生一种,所以没必要创建多个槽位来存储异常对象
- Start Length Slot Name Signature
- 9 3 2 e Ljava/lang/ArithmeticException;
- 16 3 2 e Ljava/lang/NullPointerException;
- 23 3 2 e Ljava/lang/Exception;
- 0 27 0 args [Ljava/lang/String;
- 2 25 1 i I
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- } catch (Exception e) {
- i = 20;
- } finally {
- i = 30;
- }
- }
对应字节码:
从下面字节码中我们可以看到,finally的作用是把finally中的代码快复制多分,然后分别放到try代码块后,catch代码块后(goto指令前),但是有时候catch并不能完全catch你想要的exception,所以这个字节码指令会多一个保障,就是在异常表中多捕获一个异常2 5 21 any(这个是对应下面的一条指令),和对catch多捕获一个any的异常11 15 21 any(这个是对应下面的一条指令);
- Code:
- stack=1, locals=4, args_size=1
- 0: iconst_0
- 1: istore_1
- //try块
- 2: bipush 10 //-----------try try的范围可以从异常表中查询到
- 4: istore_1
- //try块执行完后,会执行finally
- 5: bipush 30 //-----------fainal
- 7: istore_1 // 把30赋值给i,这个30的赋值是在finally代码块中的
- 8: goto 27
- //catch块
- 11: astore_2 //把异常信息放入局部变量表的2号槽位
- 12: bipush 20
- 14: istore_1
- //catch块执行完后,会执行finally
- 15: bipush 30 //-----------fainal
- 17: istore_1
- 18: goto 27
- //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
- 21: astore_3
- 22: bipush 30 //-----------fainal
- 24: istore_1
- 25: aload_3 //找到刚刚没有名字的异常
- 26: athrow //抛出这个没有名字的异常
- 27: return
- Exception table:
- from to target type
- 2 5 11 Class java/lang/Exception
- 2 5 21 any
- 11 15 21 any
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
- public class FinallyReturnTest {
-
- public static void main(String[] args) {
- int i = FinallyReturnTest.test();
- // 结果为 20
- System.out.println(i);
- }
-
- public static int test() {
- int i;
- try {
- i = 10;
- return i;
- } finally {
- i = 20;
- return i;
- }
- }
- }
- Code:
- stack=1, locals=3, args_size=0
- 0: bipush 10 //放入栈顶
- 2: istore_0 //slot 0 (从栈顶移除了)
- 3: iload_0
- 4: istore_1 // 暂存返回值
- 5: bipush 20
- 7: istore_0 //20这个值对10进行了覆盖
- 8: iload_0
- 9: ireturn // ireturn 会【返回操作数栈顶】的整型值 20
- // 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
- 10: astore_2
- 11: bipush 20
- 13: istore_0
- 14: iload_0
- 15: ireturn // 这里没有 athrow 了,也就是如果在 finally 块中如果有返回操作的话,且 try 块中出现异常,会吞掉异常!
- Exception table:
- from to target type
- 0 5 10 any
由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
所以不要在finally中进行返回操作
- public static int test() {
- int i;
- try {
- i = 10;
- // 这里应该会抛出异常
- i = i/0;
- return i;
- } finally {
- i = 20;
- return i;
- }
- }
会发现打印结果为 20 ,并未抛出异常;

但是如果我们把finally中的return给去掉,那么返回的又是什么?
- public static int test() {
- int i = 10;
- try {
- return i;
- } finally {
- i = 20; //最后的结果是返回10 !!!
- }
- }
- Code:
- stack=1, locals=3, args_size=0
- 0: bipush 10 //把10放入栈顶
- 2: istore_0 // 把10存储在局部变量表的0号槽位
- 3: iload_0 // 然后从局部变量表中把10又加载到操作数栈顶,按理说此时该返回了,但是明显没有立马返回,而是istore_1,把刚刚加载到操作数栈中的10又在局部变量表中的1号槽位备份一份
- 4: istore_1 // 加载到局部变量表的1号位置,【目的是为了固定返回值】
- 5: bipush 20 //------执行finally代码块
- 7: istore_0 // 赋值给i 20
- 8: iload_1 // 【加载局部变量表1号位置的数10到操作数栈】
- 9: ireturn // 返回操作数栈顶元素 10
- 10: astore_2
- 11: bipush 20
- 13: istore_0
- 14: aload_2 // 加载异常
- 15: athrow // 仍然会抛出异常
- Exception table:
- from to target type
- 3 5 10 any
我们发现如果在try中进行了return,那么即便finally中的变量发生了变化,那么返回的依旧是try中的变量值,因为我们可以从字节码指令看到try中的变量会先被备份一次用来返回;
- public class Demo {
- public static void main(String[] args) {
- Object lock = new Object();
- synchronized (lock) {
- System.out.println("ok");
- }
- }
- }
- 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 //第二份对象的引用赋值给局部变量表中的lock,从这里结束,第一行代码执行完毕
- 8: aload_1 //开始进入synchronized代码快,先把对象加载到操作数栈
- 9: dup //对这个lock对象的引用进行复制,分别对应加锁和解锁两个阶段使用
- 10: astore_2 //把刚刚复制出来的对象引用给存储起来到二号槽位(二号槽位是没有name的)
- 11: monitorenter //这个指令会把栈顶的对象引用给消耗掉,对lock引用所执行的对象进行加锁操作
- 12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;开始执行打印方法
- 15: ldc #4 // String ok ldc表示加载常量
- 17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 20: aload_2 //加载刚刚暂存在局部变量表的lock对象引用
- 21: monitorexit //完成解锁
- 22: goto 30
- 25: astore_3 //把异常对象的引用存储到局部变量表中的3号槽位
- 26: aload_2 //加载刚刚暂存在局部变量表的lock对象引用
- 27: monitorexit //完成解锁
- 28: aload_3 //把刚刚那个异常对象从局部变量表中加载到操作数栈中,进行抛出
- 29: athrow
- 30: return
- Exception table://如果12-22发生异常,那么就会进入到25行指令,是为了保证进入到异常后还能正常解锁
- from to target type
- 12 22 25 any
- 25 28 25 any
- LineNumberTable:
- line 11: 0
- line 12: 8
- line 13: 12
- line 14: 20
- line 15: 30
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 31 0 args [Ljava/lang/String;
- 8 23 1 lock Ljava/lang/Object;
注意:方法级别的synchronized不会在字节码指令中有所体现;
所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利;
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
- public class Candy1 {
- }
经过编译期优化后: 前提是你没有额外直接写构造方法;
- public class Candy1 {
- //这个无参构造器是java编译器帮我们加上的
- public Candy1() {
- //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
- super();
- }
- }
基本类型和其包装类型的相互转换过程,称为拆装箱,在JDK 5以后,它们的转换可以在【编译期】自动完成;
- public class Candy2 {
- public static void main(String[] args) {
- Integer x = 1;
- int y = x;
- }
- }
优化后对应的Java代码:
- public class Candy2 {
- public static void main(String[] args) {
- //基本类型赋值给包装类型,称为装箱
- Integer x = Integer.valueOf(1); //会对整数进行自动包装,范围是-128-127之间,这个阶段的整数是不用new的,内存中已经帮你创建好了,只有是没超过这个范围那么就是直接从缓存中取,超过的话才会去new
- //包装类型赋值给基本类型,称谓拆箱
- 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 o)
- Integer x = list.get(0); //实际上是调用 Object bbj = List.get(int index);
- }
- }

- 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 Demo4 {}
-
-
- public static void foo(String[] args) {
- String[] arr = args;
- System.out.println(arr.length);
- }
-
- public static void main(String[] args) {
- foo(new String[]{"hello", "world"});
- }
- }
注意,如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null;
- public class Candy5 {
- public static void main(String[] args) {
- //数组赋初值的简化写法也是一种语法糖。
- int[] arr = {1, 2, 3, 4, 5};
- for(int x : arr) { //编译之后就是for循环
- 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 static void main(String[] args) {
- List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
- for (Integer x : list) {
- System.out.println(x); //编译器优化后会变成迭代器的遍历
- }
- }
注意:集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator
- public class Candy6 {
- public Candy6 {}
-
- 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);
- }
- }
- }
- public class Demo {
- 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;
- }
- }
- }
在编译器中执行的操作:
- public class Demo {
- public Demo() {
-
- }
- 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;
- }
- }
- }
过程说明:
在编译期间,单个的switch被分为了两个
第一个用来匹配字符串,并给x赋值
字符串的匹配用到了字符串的hashCode,还用到了equals方法
使用hashCode是为了提高比较效率(尽量减少equals次数),使用equals是防止有hashCode冲突(如BM和C.)
第二个用来根据x的值来决定输出语句
- enum SEX {
- MALE, FEMALE; //这两个就是class的两个对象,和普通对象的区别是枚举类的实例是有限的,而不同类的实例对象是无限的(你可以通过new关键字来不断创建)
- }
转换后的代码:
- public final class Sex extends Enum<Sex> { //被final修饰,枚举类不能继承
- //对应枚举类中的元素
- 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};
- }
-
- //调用父类中的方法 构造方法是私有的,可以不被使用者去new新的对象
- 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);
- }
-
- }
- public class Demo {
- public static void main(String[] args) {
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println("running...");
- }
- };
- }
- }
编译器转换后的代码:
- public class Demo {
- public static void main(String[] args) {
- //用额外创建的类来创建匿名内部类对象
- Runnable runnable = new Demo$1();
- }
- }
-
- //创建了一个额外的类,实现了Runnable接口
- final class Demo$1 implements Runnable {
- public Demo$1() {}
-
- @Override
- public void run() {
- System.out.println("running...");
- }
- }
如果匿名内部类中引用了局部变量:
- public class Demo {
- public static void main(String[] args) {
- int x = 1;
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println(x);
- }
- };
- }
- }
编译器转换后的代码:
- public class Demo {
- public static void main(String[] args) {
- int x = 1;
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println(x);
- }
- };
- }
- }
-
- final class Demo$1 implements Runnable {
- //多创建了一个变量
- int val$x;
- //变为了有参构造器
- public Demo$1(int x) {
- this.val$x = x;
- }
-
- @Override
- public void run() {
- System.out.println(val$x);
- }
- }
-
-
- public class Demo {
- public static void test(final int x) { //把加final编译器会帮你默认加上的
- Runnable runnable = new Candy11$1(x);
- }
- }
注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Demo1 对象时,将 x 的值赋值给了 Demo1 对象的 值后,如果不是 final 声明的 x 值发生了改变,匿名内部类则值不一致。
将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像(桥梁作用,Java和c++之间的桥梁),例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的

instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中;
_java_mirror则是保存在堆内存中
InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址
类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
验证阶段:验证类是否符合 JVM规范,安全性检查;
准备阶段:为 static 变量分配空间(jdk8后这个静态变量在堆中),设置默认值
static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了
static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成,由类构造器cinit()来完成赋值;
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
如果 static 变量是 final 的,但属于引用类型(使用new),那么赋值会在初始化阶段完成(就是字节码的code阶段)
解析阶段:将常量池中的符号引用解析为直接引用;因为符号引用仅仅就是一个符号引用,jvm不知道它的具体含义是什么,但是经过实际解析后jvm就可以知道这个类,方法在内存中实实在在的位置了;
未解析时,常量池中的看到的对象仅是符号,未真正的存在于内存中
解析以后,会将常量池中的符号引用解析为直接引用
cinit()v:
初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的『构造方法』的线程安全
clinit()方法是由编译器自动收集【类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并】产生的;
编译器收集的顺序是由代码语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如

类初始化的发生的时机:
类的初始化的懒惰的,以下情况会初始化
main 方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化,如果父类还没初始化,会引发
子类访问父类的静态变量,只会触发父类的初始化
Class.forName
new 会导致初始化
以下情况不会初始化
访问类的 static final 静态常量(基本类型和字符串),在类的链接的准备阶段就完成了
类对象.class 不会触发初始化
创建该类对象的数组
类加载器的.loadClass方法
Class.forNamed的参数2为false时
验证类是否被初始化,可以看改类的静态代码块是否被执行: 放开自己要测试行代码的注释,运行看有没有触发下面的类A和类B ;
- public class Load1 {
- static {
- System.out.println("main init");
- }
- public static void main(String[] args) throws ClassNotFoundException {
- // 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.ali.jvm.test.classload.B");
- // 5. 不会初始化类 B,但会加载 B、A
- // ClassLoader c2 = Thread.currentThread().getContextClassLoader();
- // Class.forName("cn.ali.jvm.test.classload.B", false, c2);
-
-
- // 1. 首次访问这个类的静态变量或静态方法时
- // System.out.println(A.a);
- // 2. 子类初始化,如果父类还没初始化,会引发
- // System.out.println(B.c);
- // 3. 子类访问父类静态变量,只触发父类初始化
- // System.out.println(B.a);
- // 4. 会初始化类 B,并先初始化类 A
- // Class.forName("cn.ali.jvm.test.classload.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");
- }
- }
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
- public class Load2 {
-
- public static void main(String[] args) {
- System.out.println(E.a); //不会导致E的初始化
- System.out.println(E.b); //不会导致E的初始化
-
- System.out.println(E.c); // 会导致 E 类初始化,因为 Integer 是包装类,会自动装箱操作,Integer.valueof(20)
- }
- }
-
- class E {
- public static final int a = 10;
- public static final String b = "hello";
- public static final Integer c = 20;
-
- static {
- System.out.println("E cinit");
- }
- }
典型应用 - 完成懒惰初始化单例模式 (静态内部类): 类加载的特性,就是只有第一次使用这个类才会去加载这个类,触发类的加载链接; 只有访问这个内部类才会去加载这个类,就是你第一次去调用这个getInstance 方法的时候才会导致内部类加载和初始化其静态成员(INSTANCE); 这种方式是线程安全性的,由类加载器来保证这个单例的线程安全性;
- public class Singleton {
-
- private Singleton() { }
-
-
- // 内部类中保存单例 静态内部类可以访问外步类的所有资源
- private static class LazyHolder {
- static final Singleton INSTANCE = new Singleton();
- }
-
- // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
- public static Singleton getInstance() {
- return LazyHolder.INSTANCE;
- }
- }
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(ClassLoader);
以JDK 8为例:
| 名称 | 加载的类 | 说明 |
|---|---|---|
| Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
| Extension ClassLoader(拓展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,【显示为null】 |
| Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
| 自定义类加载器 | 自定义 | 上级为Application |
加载的过程有一个层级关系,比如这个application 类加载器加载类的时候会先去询问一下它的上级 Extension类加载器有没有加载这个类,如果没有,那么还会继续委托它的上级BootStrap加载器有没有加载这个类,如果application 类加载器的两个上级类加载器都没有加载这个类,那么此时启动类加载器会在自己可以加载的目录下寻找这个类,如果可以找到的话那么启动类加载器就会把这个类加载到内存中,然后下级的加载器就可以不用来加载这个类了,如果启动类加载器和拓展类加载器即没加载过并且在自己可加载路径中也没找到这个类,那么这个时候才能轮到application 类加载器来加载;
这种加载类型在JVM叫做双亲委派加载;
上级类加载器加载后该类对下级加载器可见 ,但是下级加载的类对上级是不可见的;
可通过在控制台输入指令,使得类被启动类加器加载;
如果classpath和JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载;
双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则;
一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给自己的上一级的加载器去执行。
如果父类(自己上一级)的加载器还存在其父类加载器(自己上一级),则进一步向上委托,依次递归,请求最终会到达顶层的启动类加载器。
如果父类加载器可以完成类加载任务(就是可以在自己可加载目录下找到这个类),就成功返回,倘若无法完成此加载任务,则委派给它的子加载器(下一级)去加载。 比如有个类加载请求来了,它会一直向上委托,直到启动类加载器(BootStrap);然后启动类加载器尝试加载,如果它不能加载,则会给他的子加载器扩展类加载器加载;如果扩展类加载器还是不能加载;则再到下一级应用程序类加载器。
loadClass源码:
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // 1.首先查找本类是否已经被该类加载器加载过了
- Class<?> c = findLoadedClass(name);
- // 如果没有被加载过
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- // 看是否被它的上级加载器加载过了 Extension 的上级是Bootstarp,但它显示为null
- if (parent != null) {
- //2.有上级的话就会委派上级 loadclass
- c = parent.loadClass(name, false);
- } else {
- // 3.看是否被启动类加载器加载过
- 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;
- }
- }
哪些情况需要到自定义类加载器?
想加载非 classpath 随意路径中的类文件
通过接口来使用实现,希望解耦时,常用在框架设计
这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤
继承 ClassLoader 父类
要遵从双亲委派机制,重写 findClass 方法
不是重写 loadClass 方法,否则不会走双亲委派机制
读取类文件的字节码
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK1.2面世以前的“远古”时代
建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的
如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的
这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
VM 将执行状态分成了 5 个层次:
0层:解释执行,用解释器将字节码翻译为机器码
1层:使用 C1 即时编译器编译执行(不带 profiling)
2层:使用 C1 即时编译器编译执行(带基本的profiling)
3层:使用 C1 即时编译器编译执行(带完全的profiling)
4层:使用 C2 即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
解释器
将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
是将字节码解释为针对所有平台都通用的机器码
即时编译器
将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter(解释器) < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),并优化这些热点代码;
C可以提升5倍左右到效率,C2可以提高大约10-100倍的效率;
全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
对象是一个静态变量
对象是一个已经发生逃逸的对象
对象作为当前方法的返回值
参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的
没有逃逸
即方法中的对象没有发生逃逸
逃逸分析优化
针对上面第三点,当一个对象没有逃逸时,可以得到虚拟机的优化;
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
逃逸分析的 JVM 参数如下:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数;
逃逸优化是在c2编译器中进行的,jit在进行逃逸分析后发现你创建的对象或者是是变量外面的类压根就用不到(比如你在循环中一直new对象),它就会帮你把创建对象的字节码给替换掉;
我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁
例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作
锁消除的 JVM 参数如下:
开启锁消除:-XX:+EliminateLocks
关闭锁消除:-XX:-EliminateLocks
锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上;
很多人将【Java内存结构】与【Java内存模型】搞混淆;
java内存模型是 Java memory model (JMM)的意思;简单的说,JMM定义了一套多线程读写共享数据时(成员变量,数组)时,对数据的可见性,有序性,和原子性的规则和保障;
参考我的另一篇博客,有更清晰的讲解:Java并发编程(中下篇)从入门到深入 超详细笔记_未来很长,别只看眼前的博客-CSDN博客