当前位置: 首页 > news >正文

黑马JVM解析笔记(五):深入理解Java字节码执行机制

1.从字节码的角度分析i++

/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {public static void main(String[] args) {int a = 10;int b = a++ + ++a + a--;System.out.println(a);System.out.println(b);}
}

a++++a 实际上代表了两个不同的操作,它们分别对应自增和取数的过程。首先需要明确的是,自增操作是在局部变量上完成的。接着,了解这两者的区别至关重要:哪一个是先取数据,哪一个是先进行自增(或自减)。

具体来说:

  • a++a-- 先取数,再进行局部自增(或自减)。
  • ++a--a 先进行局部自增(或自减),然后再取数。

因此,上述的代码最终结果是 b = 34

2.条件判断指令

在这里插入图片描述

2.1 几点说明:
  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码
2.2 案例
public class Demo3_3 {public static void main(String[] args) {int a = 0;if(a == 0) {a = 10;} else {a = 20;}
}//字节码
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12  //判断是否为0,若是跳到12行
6: bipush 10
8: istore_1
9: goto 15  //跳转
12: bipush 20
14: istore_1
15: return

3. 循环控制指令

好的,下面是经过整理和润色后的内容:

在 Java 中,循环控制结构其实是通过之前介绍的字节码指令来实现的。我们通过分析 whiledo whilefor 循环的字节码,可以更好地理解它们背后的实现原理。

3.1. while 循环字节码示例

对于以下的 while 循环代码:

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

对应的字节码是:

0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

字节码的执行流程如下:

  • 初始化变量 a(即 istore_1)。
  • 每次循环时,检查 a 是否小于 10(通过 if_icmpge 判断)。
  • 如果条件为 true,则执行 iinc(自增 a),然后跳回循环判断(goto 2)。
  • 如果条件为 false,则退出循环并结束程序。
3.2 do while 循环字节码示例

对于以下的 do while 循环代码:

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

对应的字节码是:

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

字节码的执行流程如下:

  • 初始化变量 aistore_1)。
  • 在循环内部先执行 iinc(自增 a),然后检查 a 是否小于 10。
  • 如果 a 小于 10,跳回到步骤 2,继续自增并检查条件。
  • 如果条件不满足,则退出循环并结束程序。
3.3 for 循环字节码示例

最后是 for 循环。其实,如果我们仔细观察 whilefor 循环的字节码,我们会发现它们是一模一样的。尽管语法上 for 循环包括了初始化、条件判断和自增部分,但在字节码层面,它们表现得几乎是相同的。

因此,以下是 for 循环的字节码:

public class Demo3_6 {public static void main(String[] args) {for (int a = 0; a < 10; a++) {// do something}}
}

字节码也是与 while 循环的字节码完全相同:

0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

4.判断的结果

请从字节码角度分析,下列代码运行的结果:

public class Demo3_6_1 {public static void main(String[] args) {int i = 0;int x = 0;while (i < 10) {x = x++;i++;}System.out.println(x); // 结果是 0}
}

分析结果为什么是0,是因为x++,先取数后自增,取数到操作栈依旧是0,局部变量位置是1,但是x=,这个表示复制,又把局部变量的位置改成0,这样就陷入死循环,无论循环多少次一直为0.

5.构造方法

5.1 <cint> ()v
public class StaticTest {static int i = 10;static {i = 30;}static {i = 20;}public static void main(String[] args) {}
}

编译器会按从上至下的顺序,收集所有静态代码块和静态成员赋值的代码,合并为一个特殊的方法

 Code:0: bipush        102: putstatic     #7                  // Field i:I5: bipush        307: putstatic     #7                  // Field i:I10: bipush        2012: putstatic     #7                  // Field i:I15: return
5.2 <int>()V
public class Demo3_8_2 {private String a = "s1";{b = 20;}private int b = 10;{a = "s2";}public Demo3_8_2(String a, int b) {this.a = a;this.b = b;}public static void main(String[] args) {Demo3_8_2 d = new Demo3_8_2("s3", 30);System.out.println(d.a);System.out.println(d.b);}
}

编译器会按从至下的顺序,收集所有{}代码和成员变量,形成新的构造方法,但是原始的构造放内的代码执行总是在最后面

因此这个代码的结果是:s3和30

6.方法的调用

public class Demo3_9 {
public Demo3_9() { }private void test1() { }private final void test2() { }public void test3() { }public static void test4() { }public static void main(String[] args) {Demo3_9 d = new Demo3_9();d.test1();d.test2();d.test3();d.test4();Demo3_9.test4();}
}

字节码

0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return

几种类型的方法的调用说明一下:

  • invokespecial,普通私有方法的调用
  • invokevirtual,调用实例方法,通过对象的类型决定调用哪个方法(普通方法调用)。
  • invokestatic,静态的方法调用

整个流程为:

  • new 新建对象,给对象分配堆内存,然后把对象引用压入操作数栈
  • dup,是把对象复制一份,为什么要复制一份呢,一个是调用对象构造方法,一个是把这个对象赋给局部变量
  • 然后调用方法就行
  • 备注:d.test3();这种形式调用静态方法,会有一个pop出栈的操作,这样就会把【对象引用】从操作数弹掉

7.多态是在字节码中是如何调用的

当执行 invokevirtual 指令时,

  1. 获取对象引用:从栈帧中的操作数栈中获取方法调用的对象引用。
  2. 分析对象头:通过对象引用找到对象的 Class 对象,进一步分析该对象的实际类型。
  3. 查找 vtable:通过对象的实际类型,获取其 vtable(虚方法表)。
  4. 查表得到方法地址:根据方法名和签名,在 vtable 中查找到对应的方法地址,确保方法是动态绑定的。
  5. 执行方法字节码:跳转到方法的字节码地址并执行该方法。

8.异常处理

8.1 catch
public class Demo3_11_1 {
public static void main(String[] args) {int i = 0;try {i = 10;} catch (Exception e) {i = 20;}}
}//main方法的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
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
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
StackMapTable: ...
MethodParameters: ...
}
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
8.2 finally
public class Demo3_11_4 {
public static void main(String[] args) {int i = 0;try {i = 10;} catch (Exception e) {i = 20;} finally {i = 30;}}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try --------------------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // finally |
7: istore_1 // 30 -> i |
8: goto 27 // return -----------------------------------
11: astore_2 // catch Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
21: astore_3 // catch any -> slot 3 ----------------------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw ------------------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: ...
MethodParameters: ...

由于finally必须要执行,可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程,最后一份是,怕catch捕获不到的异常,执行不了所有在其他异常也复制了一份

8.2.1finally面试题1
public class Demo3_12_2 {
public static void main(String[] args) {int result = test();System.out.println(result);public static int test() {try {return 10;} finally {return 20;}}
}//方法字节码
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> slot 0 (从栈顶移除了)
3: bipush 20 // <- 20 放入栈顶
5: ireturn // 返回栈顶 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入栈顶
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...

由这个字节码我们可以看到,finally里面的逻辑一定会执行,但是如果 finally 中有 return,它会覆盖任何异常或者其他的返回值。

8.2.2 finally面试题2
public class Demo3_12_2 {public static void main(String[] args) {int result = test();System.out.println(result);}public static int test() {int i = 10;try {return i;} finally {i = 20;}}
}
//字节码
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
5: bipush 20 // <- 20 放入栈顶
7: istore_0 // 20 -> i
8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
9: ireturn // 返回栈顶的 int(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
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I

通过使用 istore_1 暂存 i 的值,字节码确保了即使在 finally 块中修改了 i 的值(将其设置为 20局部变量),最终返回的仍然是 try 块中的原始 i 值(即 10)。

8.2.2 finally面试题2
public class Demo3_12_2 {public static void main(String[] args) {int result = test();System.out.println(result);}public static int test() {int i = 10;try {return i;} finally {i = 20;}}
}
//字节码
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
5: bipush 20 // <- 20 放入栈顶
7: istore_0 // 20 -> i
8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
9: ireturn // 返回栈顶的 int(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
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I

istore_1,可以把值固定住

9.synchronnized

public class Demo3_13 {public static void main(String[] args) {Object lock = new Object();synchronized (lock) {System.out.println("ok");}}
}//字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V
7: astore_1 // lock引用 -> lock
8: aload_1 // <- lock (synchronized开始)
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: ...
MethodParameters: ...

几点说明:

  • 为什么字节码第9行要把锁的引用复制两份,因为是需要给两个操作,一个加锁,一个解锁
  • 还有就是异常捕获,由于对程序进行加锁,如果在执行程序的过程发生什么异常,导致程序没有解锁,这样就会程序就会出问题。因此要设置异常捕获,如果出现异常,就还是要进行解锁,再抛出异常
  • 方法级别的 synchronized 不会在字节码指令中有所体现

相关文章:

  • 1 Studying《Is Parallel Programming Hard》6-9
  • 飞算科技依托 JavaAI 核心技术,打造企业级智能开发全场景方案
  • 杭州市长姚高员带队调研景联文科技,听取高质量数据集建设情况
  • 论基于架构的软件设计方法(ABSD)及应用
  • oracle集合一 关联数组(索引表)学习
  • 秘窟燃战.纷魄凌霄(第二集)
  • 关于前端npm install安装依赖和打包的一些问题记录
  • Spring-图书管理系统
  • ZLG嵌入式笔记 | 工业现场掉电,系统异常如何破解?
  • 26考研|数学分析:隐函数定理及其应用
  • 命名数据网络 | 签名(Signature)
  • php flush实时输出线上环境好使,本地环境等待一段时间后一次性输出结果的原因
  • PR2020+MS1861 AHD转MIPI(DSI)/LVDS/TTL转换器
  • 排查 WebView 中 touch、click 事件失效:移动端调试过程详解
  • JUC:4.线程常见操作与两阶段终止模式
  • 【图像处理入门】12. 综合项目与进阶:超分辨率、医学分割与工业检测
  • Godot4.3类星露谷游戏开发之【简易库存】(UI部分)
  • 一文详解 transformer 中的 self-attention
  • 数星星--二分
  • AI Agent全解析:定义、原理与B2B企业中的应用落地指南