本篇博客记录个人学习jvm的过程,如有错误,敬请指正
文章目录
一、类文件结构
由于类文件结构过于抽象,都是16进制的的数字组成,作者学习水平有限,暂时无法学习,可以参考下面视频
黑马jvm视频
比起看类文件结构,我们可以通过对.class文件进行反编译查看
javap -v 类名.class (对class文件进行反编译)
下面可以通过一个个简单的demo了解方法执行流程
二、字节码简单案例
1.案例1(基本执行流程)
public class Demo1 {
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:/javaTest/ThreadDemo1/out/production/ThreadDemo1/com/atguigu/test8/Demo1.class
Last modified 2022年7月11日; size 613 bytes
SHA-256 checksum 4307a5c9346bae1dfb88c6c681ac749b46970970618fed085b66bd1573f5f99b
Compiled from "Demo1.java"
public class com.atguigu.test8.Demo1
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #6 // com/atguigu/test8/Demo1
super_class: #7 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
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 // com/atguigu/test8/Demo1
#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 Lcom/atguigu/test8/Demo1;
#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 Demo1.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 com/atguigu/test8/Demo1
#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 com.atguigu.test8.Demo1();
descriptor: ()V
flags: (0x0001) 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 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/atguigu/test8/Demo1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) 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 15: 0
line 16: 3
line 17: 6
line 18: 10
line 19: 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: "Demo1.java"
常量池载入运行时常量池(属于方法区)
字节码放入方法区
注意:当数的大小超过short类型的最大值时,它会存储在常量池中
下面局部变量大小为4,操作数栈大小为2
下面开始执行方法区中的字节码,第一行是bipush
- bipush :将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
下一行是istore 1,代表的是将操作数栈顶数据弹出,存入局部变量表的slot(槽位)1
ldc #3代表将运行时常量池中的#3 所指代的数据放入栈中
istore2代表将栈顶元素放入slot2槽位
iload1和iload2代表将槽位1和槽位2读取到操作数栈中
iadd将操作数栈中的两个元素弹出栈并相加,并加运算结果在压入操作数栈中
istore 3放入3号槽位
getstatic #4:在运行时常量池中找到#4,发现是一个对象;在堆内存中找到该对象,将其引用放入操作数栈中;
iload 3将3号槽位中的数读入到栈中
invokevirtual 5:
- 找到常量池 #5 项
- 定位到方法区 java/io/PrintStream.println:(I)V 方法
- 发现是方法后,由虚拟机分配一个新的栈帧
- 传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除 main 操作数栈内容
return
弹出 main 栈帧,程序结束
2.案例2(i = i++)
public class Test {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
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
3.案例3(分析方法调用)
public class Demo1 {
public Demo1() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
demo1.test1(); //私有
demo1.test2(); //final
demo1.test3(); //公共
Demo1.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
4.案例4(多态)
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的 Class
- Class 结构中有 vtable
- 查询 vtable 找到方法的具体地址
- 执行方法的字节码
5.案例5(异常处理)
public class Demo1 {
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 )
6.案例6(finally)
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
对应字节码如下:
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
通过上面的字节码可知,其实是分为三个分支,try,catch,还有catch未捕获到的异常,这三个分支后面其实都有一份finally
7.案例7(finally中return)
public class Demo1 {
public static void main(String[] args) {
int i = Demo1.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 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
我们将finally中的return去除:
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
8.案例8(Synchronized)
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 //把异常对象(e)的引用存储到局部变量表中的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;
三、类加载阶段
类加载主要分为以下阶段
1.加载
类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
如果这个类还有父类没有加载,先加载父类
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了
2.验证
3.准备
为 static 变量分配空间(jdk8后这个静态变量在堆中),设置默认值
定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字 public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。
4.解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量
符号引用仅仅就是一个符号引用,jvm不知道它的具体含义是什么,但是经过实际解析后jvm就可以知道这个类,方法在内存中实实在在的位置了
5.初始化
初始化阶段是执行初始化方法 clinit()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。虚拟机会保证这个类的的线程安全
对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
1、当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
- 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
- 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
2、使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(“…”), newInstance() 等等。如果类没初始化,需要触发其初始化。
3、初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
4、main方法所在的类,虚拟机会先初始化这个类。
5、MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
三、类加载阶器
所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存 注意:数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
名称 | 加载的类 | 说明 |
---|---|---|
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
双亲委派模式
双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则
- 类加载器收到了类加载请求,它会首先委托上级类加载器去执行
- 如果上一级加载器还存在上级,那么会继续请求,直到达到启动类加载器
- 如果父类加载器可以完成类加载任务,那么直接返回,否则委派给下级
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;
}
}
双亲委派模型的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
自定义类加载器
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。想打破双亲委派机制的话需要重写loadClass() 方法