入门
接着上一节,研究一下两组字节码指令,一个是
public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
-
2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
-
b7 => invokespecial 预备调用构造方法,哪个方法呢?
-
00 01 引用常量池中 #1 项,即【 Method java/lang/Object.""😦)V 】
-
b1 表示返回
另一个是 public static void main(java.lang.String[]); 主方法的字节码指令
-
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.println:(Ljava/lang/String;)V】
-
b1 表示返回
请参考
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
javap工具
图解方法执行流程
原始java代码
编译后的字节码文件
常量池载入运行时常量池
方法字节码载入方法区
main线程开始运行,分配栈帧内存
(stack=2,local=4)
- stack 操作数栈深度,即只允许栈内两个操作数
- local即本地变量表,即能为本地变量分配四个槽位
执行引擎开始执行字节码
bipush10
-
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
-
sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
-
ldc 将一个 int 压入操作数栈
-
ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
-
这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore_1
- 将操作数栈顶数据弹出,存入局部变量表的 slot 1
ldc #3
-
从常量池加载 #3 数据到操作数栈
-
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore_2
iload_1
iload_2
iadd
istore_3
getstatic #4
iload_3
invokevirtual #5
-
找到常量池 #5 项
-
定位到方法区 java/io/PrintStream.println:(I)V 方法
-
生成新的栈帧(分配 locals、stack等)
-
传递参数,执行新栈帧中的字节码
-
执行完毕,弹出栈帧
-
清除 main 操作数栈内容
return
-
完成 main 方法调用,弹出 main 栈帧
-
程序结束
练习:分析i++
源码
字节码
分析
-
注意 iinc 指令是直接在局部变量 slot 上进行运算
-
a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
条件判断指令
二进制 | 字节码 | 含义 |
---|---|---|
0x99 | ifeq | 判断是否 == 0 |
0x9a | ifne | 判断是否 !=0 |
0x9b | iflt | 判断是否 < 0 |
0x9c | ifge | 判断是否 >= 0 |
0x9d | ifgt | 判断是否 > 0 |
0x9e | ifle | 判断是否 <= 0 |
0x9f | if_icmpeq | 两个int是否 == |
0xa0 | if_icmpne | 两个int是否 != |
0xa1 | if_icmplt | 两个int是否 < |
0xa2 | if_icmpge | 两个int是否 >= |
0xa3 | if_icmpgt | 两个int是否 > |
0xa4 | if_icmple | 两个int是否 <= |
0xa5 | if_acmpeq | 两个引用是否 == |
0xa6 | if_acmpne | 两个引用是否 != |
0xc6 | ifnull | 判断是否 == null |
0xc7 | ifnonnull | 判断是否 != null |
几点说明:
-
byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
-
goto 用来进行跳转到指定行号的字节码
源码
字节码
循环控制指令
while循环
源码
字节码
do while循环
源码
字节码
- 之前案例的a++先iload在iinc,是因为在自增之前有赋值操作,需要放如操作数栈
- a = a++,由于先有 = 的赋值操作,所以先把a的值放到操作数栈,iload,iinc
- 此处只是a++,所以只是iinc
- 只是由于后面有 < 比较操作,所以有iload操作
for循环
源码
for循环字节码
注意
比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归
练习-判断结果
源码
- x = 0,会将0赋值给x所在本地变量表的某个槽位
- 由于是x++,所以是先把x的值从槽位拷贝到操作数栈;然后x再在槽位上执行自增变成1
- 在操作数栈中,对x进行赋值操作 x = x++,操作数栈中x++的值是0,再重新赋值给x
- x的值从自增的1,被赋值操作覆盖成了0
- 所以不管循环赋值多少次,都是0
构造方法
< cinit >()V
静态代码块和静态成员变量的初始化
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 < cinit >()V :
< cinit >()V 方法会在类加载的初始化阶段被调用
< init >()V
代码块和成员变量初始化
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
方法调用
字节码
-
new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
-
dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “< init >”😦)V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
-
最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
-
普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
-
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
-
比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
-
还有一个执行 invokespecial 的情况是通过 super 调用父类方法
多态的原理
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 执行
4)查看对象内存结构
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是MarkWord,后 8 字节就是对象的 Class 指针,但目前看不到它的实际地址
5)查看对象 Class 的内存地址
可以通过 Windows -> Console 进入命令行模式,执行
mem 0x00000001299b4978 2
mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
结果中第二行 0x000000001b7d4028 即为 Class 的内存地址
6)查看类的 vtable
- 方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面
- 方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果
无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,fifinal,static 不会列入)
那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:
通过 Windows -> Console 进入命令行模式,执行
mem 0x000000001b7d41e0 6
就得到了 6 个虚方法的入口地址
7)验证方法地址
通过 Tools -> Class Browser 查看每个类的方法定义,比较可知
对号入座,发现
-
eat() 方法是 Dog 类自己的
-
toString() 方法是继承 String 类的
-
fifinalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
8)小结
当执行 invokevirtual 指令时,
-
先通过栈帧中的对象引用找到对象
-
分析对象头,找到对象的实际 Class
-
Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
-
查表得到方法的具体地址
-
执行方法的字节码
异常处理
try-catch
源码
字节码
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
多个single-catch块的情况
源码
字节码
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
multi-catch的情况
源码
字节码
finally
源码
字节码
可以看到 fifinally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
练习-finally面试题
finally出现了return
源码
- 输出20
字节码
-
输出20
-
由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
-
至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
-
跟上例中的 fifinally 相比,发现没有 athrow 了,这告诉我们:如果在 fifinally 中出现了 return,会吞掉异常,可以试一下下面的代码
finally对返回值的影响
源码
字节码
synchronized
源码
- 如何保证synchronize在遇到异常时也能正常解锁(通过异常处理)
字节码