JVM_4_字节码指令

入门

接着上一节,研究一下两组字节码指令,一个是

public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

image-20210917110912675

  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数

  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?

  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】

  4. b1 表示返回

另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

image-20210917111213650

  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?

  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】

  3. 12 => ldc 加载参数,哪个参数呢?

  4. 03 引用常量池中 #3 项,即 【String hello world】

  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?

  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】

  7. b1 表示返回

请参考

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

javap工具

image-20210923153557134

image-20210923153623601

image-20210923153708752

图解方法执行流程

原始java代码

image-20210924111210754

编译后的字节码文件

image-20210924111241397

image-20210924111316540

image-20210924111340332

image-20210924111409302

常量池载入运行时常量池

image-20210924111440781

方法字节码载入方法区

image-20210924111506534

main线程开始运行,分配栈帧内存

(stack=2,local=4)

image-20210924111608159

  • stack 操作数栈深度,即只允许栈内两个操作数
  • local即本地变量表,即能为本地变量分配四个槽位

执行引擎开始执行字节码

bipush10

  • 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)

  • ldc 将一个 int 压入操作数栈

  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)

  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

image-20210924112034863

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1

image-20210924112215894

image-20210924112234290

ldc #3

  • 从常量池加载 #3 数据到操作数栈

  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

image-20210924112356501

istore_2

image-20210924112955648

iload_1

image-20210924113031356

iload_2

image-20210924113108640

iadd

image-20210924113139808

istore_3

image-20210924113229028

getstatic #4

image-20210924113302422

image-20210924113321963

iload_3

image-20210924113350170

invokevirtual #5

  • 找到常量池 #5 项

  • 定位到方法区 java/io/PrintStream.println:(I)V 方法

  • 生成新的栈帧(分配 locals、stack等)

  • 传递参数,执行新栈帧中的字节码

image-20210924113452774

  • 执行完毕,弹出栈帧

  • 清除 main 操作数栈内容

image-20210924113515490

return

  • 完成 main 方法调用,弹出 main 栈帧

  • 程序结束

练习:分析i++

源码

image-20210924165626742

字节码

image-20210924165651480

image-20210924165704812

分析

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算

  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

image-20210924165807165

image-20210924165908486

image-20210924170057846

image-20210924170221454

image-20210924170548957

image-20210924170650885

image-20210924170740778

image-20210924170906586

image-20210924170954460

image-20210924171036917

image-20210924171109858

条件判断指令

二进制字节码含义
0x99ifeq判断是否 == 0
0x9aifne判断是否 !=0
0x9biflt判断是否 < 0
0x9cifge判断是否 >= 0
0x9difgt判断是否 > 0
0x9eifle判断是否 <= 0
0x9fif_icmpeq两个int是否 ==
0xa0if_icmpne两个int是否 !=
0xa1if_icmplt两个int是否 <
0xa2if_icmpge两个int是否 >=
0xa3if_icmpgt两个int是否 >
0xa4if_icmple两个int是否 <=
0xa5if_acmpeq两个引用是否 ==
0xa6if_acmpne两个引用是否 !=
0xc6ifnull判断是否 == null
0xc7ifnonnull判断是否 != null

几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节

  • goto 用来进行跳转到指定行号的字节码

源码

image-20210925105301625

字节码

image-20210925105352387

循环控制指令

while循环

源码

image-20210925105448736

字节码

image-20210925105516430

do while循环

源码

image-20210925105600444

字节码

image-20210925110919606

  • 之前案例的a++先iload在iinc,是因为在自增之前有赋值操作,需要放如操作数栈
  • a = a++,由于先有 = 的赋值操作,所以先把a的值放到操作数栈,iload,iinc
  • 此处只是a++,所以只是iinc
  • 只是由于后面有 < 比较操作,所以有iload操作

for循环

源码

image-20210925105642347

for循环字节码

image-20210925105657311

注意

比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归

练习-判断结果

源码

image-20210925105819870

  • x = 0,会将0赋值给x所在本地变量表的某个槽位
  • 由于是x++,所以是先把x的值从槽位拷贝到操作数栈;然后x再在槽位上执行自增变成1
  • 在操作数栈中,对x进行赋值操作 x = x++,操作数栈中x++的值是0,再重新赋值给x
  • x的值从自增的1,被赋值操作覆盖成了0
  • 所以不管循环赋值多少次,都是0

构造方法

< cinit >()V

静态代码块和静态成员变量的初始化

image-20210925162751694

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

image-20210925162904136

< cinit >()V 方法会在类加载的初始化阶段被调用

< init >()V

代码块和成员变量初始化

image-20210925163022085

image-20210925163034579

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

image-20210925163202818

方法调用

image-20210925163503713

字节码

image-20210925163526434

  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈

  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “< init >”😦)V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量

  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定

  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态

  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】

  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了

  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

多态的原理

image-20210925170159436

1)运行代码

停在 System.in.read() 方法上,这时运行 jps 获取进程 id

2)运行 HSDB 工具

进入 JDK 安装目录,执行

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面 attach 进程 id

3)查找某个对象

打开 Tools -> Find Object By Query

输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

image-20210925170422238

4)查看对象内存结构

点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针,但目前看不到它的实际地址

image-20210925170554237

5)查看对象 Class 的内存地址

可以通过 Windows -> Console 进入命令行模式,执行

mem 0x00000001299b4978 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

结果中第二行 0x000000001b7d4028 即为 Class 的内存地址

image-20210925170802243

6)查看类的 vtable

  • 方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面

image-20210925170842197

  • 方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果

image-20210925170914079

无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,fifinal,static 不会列入)

那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:

image-20210925170950898

通过 Windows -> Console 进入命令行模式,执行

mem 0x000000001b7d41e0 6

image-20210925171027439

就得到了 6 个虚方法的入口地址

7)验证方法地址

通过 Tools -> Class Browser 查看每个类的方法定义,比较可知

image-20210925171108398

对号入座,发现

  • eat() 方法是 Dog 类自己的

  • toString() 方法是继承 String 类的

  • fifinalize() ,equals(),hashCode(),clone() 都是继承 Object 类的

8)小结

当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象

  2. 分析对象头,找到对象的实际 Class

  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了

  4. 查表得到方法的具体地址

  5. 执行方法的字节码

异常处理

try-catch

源码

image-20210925193148430

字节码

image-20210925193304497

image-20210925193317991

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

多个single-catch块的情况

源码

image-20210925193411730

字节码

image-20210925193438077

image-20210925193450388

  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

multi-catch的情况

源码

image-20210925193530484

字节码

image-20210925193553253

image-20210925193605329

finally

源码

image-20210925193627842

字节码

image-20210925193652990

image-20210925193705582

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

练习-finally面试题

finally出现了return

源码

image-20210925193812575

  • 输出20

字节码

image-20210925193839362

image-20210925193851730

  • 输出20

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准

  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子

  • 跟上例中的 fifinally 相比,发现没有 athrow 了,这告诉我们:如果在 fifinally 中出现了 return,会吞掉异常,可以试一下下面的代码

image-20210925193952176

finally对返回值的影响

源码

image-20210925194018660

字节码

image-20210925194154627

synchronized

源码

image-20210925194245536

  • 如何保证synchronize在遇到异常时也能正常解锁(通过异常处理)

字节码

image-20210925194342854

image-20210925194405057

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值