2.字节码指令
2.1 入门
jvm的解释器可以识别平台无关的字节码指令,解释为机器码执行。
2a b7 00 01 b1
this . init() return
准备了System.out对象,准备了参数“hello world”,准备了对象的方法println(String)V,并return
2.2 javap 工具
这里常量池直接把查询结果放在了右边。
有了javap,终于不用看那狗屎字节码文件了
这部分是方法信息里的init构造方法。
main方法
有参数类型,访问类型,方法属性,局部变量表,方法参数信息等等
2.3 图解方法执行流程
1)原始 java 代码
b是Short类型的最大值+1。
2)编译后的字节码文件
3)常量池载入运行时常量池
java代码执行时会由java虚拟机的类加载器将main方法所在类进行类加载。
类加载就是把class文件里的字节数据读取到内存里面。
运行时常量池是方法区的组成部分
32768对应的是源代码里面的short.value+1,比较小的数字不会存储在常量池,比如10,10会跟着方法的字节码指令存一起,一旦数字范围超过整数最大值就会存储在常量池中。
4)方法字节码载入方法区
5) main 线程开始运行,分配栈帧内存
绿色区域是局部变量表,蓝色是操作数。字节码指令里面有一个栈的最大深度和局部变量表的长度。这两项决定了栈帧内存大小。
6)执行引擎开始执行字节码
bipush 10
istore_1
然后a被复制为10,对应操作就是a=10
1dc #3
istore 2
iload1
局部变量表中不能执行a+b的操作,所以,执行引擎会对它们进行一个读取。
iload2
iadd
iadd操作是将操作数栈中的两个数取出来相加再放回去。
istore 3
给c赋值
getstatic #4
这个是到常量池中找一个成员变量的引用。这里是找System.out。找到之后不是把对象放入操作数栈,而是获取引用
iload_3
执行打印需要参数c,所以将局部变量表中c的值32778读入到操作数栈
invokevirtual #5
在常量池中找到5号条目,执行println(I)方法,参数 l 表示整数。
println方法执行时会找到方法区中新的方法,然后分配一个新的栈帧
执行时会把32778作为参数传递给新的栈帧中作为整数参数完成打印操作。
return
2.4练习-分析a++
分析
a++执行的指令是iinc,但是这玩意直接原地拉屎变大,不需要进栈又出栈。
a++是iload先,++a是iinc先
iinc的两个参数,一个是要对哪个槽位做自增,一个是自增多少。
到这里第一个加法结束了。
2.5 条件判断指令
较小的数用iconst表示,-1到5的数
ifne 不成立时会跳转到12行,压20入栈然后往下执行。
成立时就直接往下执行然后到goto跳过12,14,直接返回
2.6 循环控制指令
依旧是if+goto
if_icmpge判断是否大于等于10,是的话就跳到14
do_while
for循环
2.7 练习 - 判断结果
先load,再iinc,最后istore,又把栈里的load到的数据放回去了。
先把0读进操作数栈的,然后局部变量表上x自增1变成,然后将局部变量表上的0赋值给x.
2.8 构造方法
1) <cinit>()v
静态变量初始化,静态代码块从上到下依次执行
cinit是整个类的构造方法。
2) <init>()V
init是实例的构造方法。
从上到下有成员变量,初始化代码块,有参构造。
最后答案是s3 30
2.9方法调用
类中从上到下有构造方法,私有方法,final方法,公共方法,静态方法。
通过对象和类型都调用了静态方法
有三种指令出现。
invokevirtual是动态绑定,公共方法有可能子类也有可能父类,要在运行时确定。
另外两个都是静态绑定,可以直接找到执行地址。
执行new的时候会先分配内存给它,然后把对象的引用放到操作数栈。
dup是对栈顶的复制,操作数栈里有两个相同的引用。
invokespecial会根据栈顶的引用调用对应的构造方法。 这里消耗一个引用
astore_1会根据剩余的引用出栈存储到局部变量表中。这里消耗一个引用
执行到d.test4()时先把引用放入栈,但由于静态方法不需要引用,所以又pop弹出了,然后用invokestatic执行test4了。
要调用静态方法直接用类名去调用即可,否则会产生多余的虚拟机指令。
2.10 多态的原理
在上面有一个invokevirtual不能直接找到地址去执行,需要找到是父类还是子类。
研究invokevirtual就是研究多态的原理
面试高频题:讲讲什么是多态,底层原理是啥
1) 运行代码
64位操作系统为了节省内存用了指针压缩,查看地址时要换算不方便
代码中一个Animal父类,一个抽象方法eat(),一个toString方法,还有两个子类继承了Animal父类。
在公共类中有一个test方法,参数是Animal,这里产生了多态,实际对象可能是cat,也可能是dog.
2)运行 HSDB 工具
JDK9之后,执行JHSDB HSDB
3)找某个对象
查询语句和sql语句十分相似,这里通过查询类型别名查到dog对象在内存中的地址。
这里虚拟机中只有一个dog对象,所以查到的是唯一dog.
4)查看对象内存结构
5) 查看对象 Class 的内存地址
第二行是类型指针,用类型指针继续查
上面是类的结构。
6)查看类的 vtable
接着找类中的方法,多态的方法存在vtable虚方法表中。在类结构的最后。
是该类的地址加上1B8就是vtable地址。vtable长度是6。
然后看见6个支持重写的方法的入口地址。
7)验证方法地址
在虚方法表中的最后一个方法对应的就是dog类的eat方法。
因为dog类没有重写父类,用的是Animal的toString方法。
方法表中的第一项有Object的finalize,clone,equals方法 ,因为Animal和dog都没有重写这些方法。
8) 小结
一句话总结:invokevirtual指令调用的对象vtable中的方法
类加载阶段指的是 验证、准备、解析 这三个阶段,即加载之后,初始化之前、
2.11 异常处理
try-catch
2 4 是try中的内容, try中如果没有发生异常,就会直接去到12了。
借助Exception table异常表检测[2~5)行内容,不包含5,2~4有异常会先跟声明的异常是否一致或是子类异常,是的话就会进入第8行。
第8行astore_2是把异常对象的引用地址存储下面局部变量的e槽位上,就是catch那一行的执行。
9,11就是i=20.
多个single-catch 块的情况
与try_catch的相差不多。
multi-catch 的情况
直接一个catch块捕获多个异常。
finally
字节码
因为有可能发生Exception父类的异常,所以异常表里面又多了别的异常类型。
finally的工作方法就是将finally中的东西放在每一个分支后面,在reutrn前面,以此保证一定会被执行。
因此有三个分支,一个是try,一个是Exception的catch,一个是非Exception的catch.
2.12 练习 - finally 面试题
finally 出现了 return
正确答案是20。
简单来说,就是即使报了异常,但是最后还是执行finally例的ireturn,不会执行athrow
抛异常和返回值,这两个是冲突的,只能执行一个,平时绝对不能在finally中return
该代码最后正常执行完没有除零报错。
finally 对返回值影响
最后返回的是10
2.13 synchronized
new时复制了一份,一个用于调用构造方法,一个是给了局部变量表。
synchronized(lock)先将lock引用加载到操作数栈,然后又复制了一份。
待会一个给monitorenter 指令用,一个给了monitorexit指令用,加锁和解锁。
然后一个lock存入局部变量表2槽,剩下那个给了monitorenter 加锁。
然后12~17是打印指令
20~22是把2号槽位的引用加载到栈顶并给monitorexit解锁,然后goto return正常结束。
如果12~22有异常会进到25.
25将错误保存到局部变量表。26~27将lock引用给monitorexit解锁。
28,29加载异常类抛出。