上篇文章深入浅出JVM(九)之字节码指令(上篇)已经深入浅出说明加载存储、算术、类型转换的字节码指令,本篇文章作为字节码的指令的下篇,深入浅出的解析各种类型字节码指令,如:方法调用与返回、控制转义、异常处理、同步等
使用idea中的插件jclasslib查看编译后的字节码指令
方法调用与返回指令
方法调用指令
非虚方法: 静态方法,私有方法,父类中的方法,被final修饰的方法,实例构造器
与之对应不是非虚方法的就是虚方法了
- 普通调用指令
invokestatic
: 调用静态方法invokespecial
: 调用私有方法,父类中的方法,实例构造器方法,final方法invokeinterface
: 调用接口方法invokevirtual
: 调用虚方法
- 使用
invokestatic
和invokespecial
指令的一定是非虚方法
使用invokeinterface
指令一定是虚方法(因为接口方法需要具体的实现类去实现)
使用invokevirtual
指令可能是虚方法 - 动态调用指令
invokedynamic
: 动态解析出需要调用的方法再执行
- jdk 7 出现
invokedynamic
,支持动态语言
测试虚方法代码
- 父类
public class Father { public static void staticMethod(){ System.out.println("father static method"); } public final void finalMethod(){ System.out.println("father final method"); } public Father() { System.out.println("father init method"); } public void overrideMethod(){ System.out.println("father override method"); } }
- 接口
public interface TestInterfaceMethod { void testInterfaceMethod(); }
- 子类
public class Son extends Father{ public Son() { //invokespecial 调用父类init 非虚方法 super(); //invokestatic 调用父类静态方法 非虚方法 staticMethod(); //invokespecial 调用子类私有方法 特殊的非虚方法 privateMethod(); //invokevirtual 调用子类的重写方法 虚方法 overrideMethod(); //invokespecial 调用父类方法 非虚方法 super.overrideMethod(); //invokespecial 调用父类final方法 非虚方法 super.finalMethod(); //invokedynamic 动态生成接口的实现类 动态调用 TestInterfaceMethod test = ()->{ System.out.println("testInterfaceMethod"); }; //invokeinterface 调用接口方法 虚方法 test.testInterfaceMethod(); } @Override public void overrideMethod(){ System.out.println("son override method"); } private void privateMethod(){ System.out.println("son private method"); } public static void main(String[] args) { new Son(); } }
方法返回指令
方法返回指令: 方法结束前,将栈顶元素(最后一个元素)出栈 ,返回给调用者
根据方法的返回类型划分多种指令
操作数栈管理指令
通用型指令,不区分类型
- 出栈
pop/pop2
出栈1个/2个栈顶元素
- 入栈
dup/dup2
复制栈顶1个/2个slot并重新入栈dup_x1
复制栈顶1个slot并插入到栈顶开始的第2个slot下dup_x2
复制栈顶1个slot并插入到栈顶开始的第3个slot下dup2_x1
复制栈顶2个slot并插入到栈顶开始的第3个slot下dup2_x2
复制栈顶2个slot并插入到栈顶开始的第4个slot下
- 插入到具体的slot计算: dup的系数 +
_x
的系数
控制转义指令
条件跳转指令
通常先进行比较指令,再进行条件跳转指令
比较指令比较结果-1,0,1再进行判断是否要跳转
条件跳转指令: 出栈栈顶元素,判断它是否满足条件,若满足条件则跳转到指定位置
注意: 这种跳转指令一般都"取反",比如代码中第一个条件语句是d>100,它第一个条件跳转指令就是ifle
小于等于0,满足则跳转,不满足则按照顺序往下走
比较条件跳转指令
比较条件跳转指令 类似 比较指令和条件跳转指令 的结合体
多条件分支跳转指令
多条件分支跳转指令是为了switch-case提出的
tableswitch
用于case值连续的switch多条件分支跳转指令,效率好
lookupswitch
用于case值不连续的switch多条件分支跳转指令(虽然case值不连续,但最后会对case值进行排序)
tableswitch
lookupswitch
对于String类型是先找到对应的哈希值再equals比较确定走哪个case的
无条件跳转指令
无条件跳转指令就是跳转到某个字节码指令处
goto
经常使用
jsr,jsr_w,ret
不怎么使用了
异常处理指令
throw抛出异常对应athrow
: 清除该操作数栈上所有内容,将异常实例压入调用者操作数栈上
使用try-catch/try-final/throws时会产生异常表
异常表保存了异常处理信息 (起始、结束位置、字节码指令偏移地址、异常类在常量池中的索引等信息)
athrow
异常表
异常还会被压入栈或者保存到异常表中
同步控制指令
synchronized作用于方法时,方法的访问标识会有ACC_SYNCHRONIZED表示该方法需要加锁
synchronized作用于某个对象时,对应着**monitorentry
加锁字节码指令和 monitorexit
解锁字节码指令**
Java中的synchronized默认是可重入锁
- 当线程要访问需要加锁的对象时 (执行monitorentry)
- 先查看对象头中加锁次数,如果为0说明未加锁,获取后,加锁次数自增
- 如果不为0,再查看获取锁的线程是不是自己,如果是自己就可以访问,加锁次数自增
- 如果不为0且获取锁线程不是自己,就阻塞
当线程释放锁时 (执行monitorexit)会让加锁次数自减
为什么会有2个monitorexit ?
程序正常执行应该是一个monitorentry对应一个monitorexit的
如果程序在加锁的代码中抛出了异常,没有释放锁,那不就会造成其他阻塞的线程永远也拿不到锁了吗
所以在程序抛出异常时(跳转PC偏移量为15的指令)继续往下执行,抛出异常前要释放锁
总结
本篇文章作为字节码指令的下篇,深入浅出的解析方法调用与返回,操作数栈的入栈、出栈,控制转义,异常和同步相关字节码指令
方法调用指令分为静态、私有、接口、虚、动态方法等,返回指令则主要是以i、l、f、d、a开头的return指令分别处理不同类型的返回值
操作数栈中的出栈指令常用pop
相关指令,入栈(复制栈顶元素并插入)常用dup
相关指令
控制转义指令中条件跳转指令是判断栈顶元素来进行跳转,比较条件跳转指令是通过两个栈顶元素比较来判断跳转,多条件分支跳转是满足switch,常在异常时进行goto
无条件跳转
异常处理指令用于抛出异常,清除操作数栈并将异常压入调用者操作数栈顶
同步控制指令常使用monitorentry
和monitoryexit
,为了防止异常时死锁,抛异常前执行monitoryexit
最后
- 参考资料
- 《深入理解Java虚拟机》
本篇文章将被收入JVM专栏,觉得不错感兴趣的同学可以收藏专栏哟~
觉得菜菜写的不错,可以点赞、关注支持哟~
有什么问题可以在评论区交流喔~