Jvm字节码指令答疑

前言

回想起当初自己刚学字节码指令时,看的文章资料倒是不少,但是几乎都差不多,翻来覆去说的都是差不多一样的东西,自己有许多疑问无法理解,这些疑问也没办法在网上搜(这让我深刻体会到了上学时老师的重要性(#°Д°)),那感觉太痛苦了。所以在此记录一下自己当时的疑问,希望对有缘看到这篇文章的小伙伴有所帮助。

1、局部变量表中变量个数问题

每个栈帧中的局部变量表的最大容量是在编译时期就被确定的,除了局部变量存储在局部变量表中,方法的参数也会存在局部变量表中。如果方法不是静态方法,那么方法所在类的对象引用也会存储在局部变量表中,并且是存在第一个槽中,名称是this。在this后面存的是方法的参数,在方法参数后面才是存的方法里的局部变量。

槽:即slot,局部变量表的容量以槽为最小单位,一个槽可以容纳一个 32 位的数据类型(比如int)。

例如如下方法:

arduino 复制代码
public class Test {
    int add(int a,int b){
       int value = a + b;
       return value;
    }
}

该方法栈帧的局部变量表最大容量为4,里面有4个变量:

如果该方法是静态方法,那么就没有this,局部变量表的最大容量为3。

另外局部变量表并不一定是一直都是存满的。作用域为整个方法域的局部变量是一直存于局部变量表中,但是作用域为方法中某一个代码块的变量(为便于理解这里称之为临时变量)是只有使用时才会存入局部变量表,并且离开临时变量的作用域后会从局部变量表中删除该临时变量,所以下一个临时变量也会被存于该临时变量呆过的槽中。

例如:

arduino 复制代码
public class Test {
    int add(int a,int b,boolean boo){
        int value = a + b;
        {
            int c = 8;
        }
        if (boo){
            int d = value;
        }
        return value;
    }
}

思考一下上面的文件编译后,局部变量表最大容量是多少? 局部变量有this、a、b、boo、value、c、d共7个,最大容量是7?

用javap命令看一下,打印如下:

由上图可知,临时变量c和d都可存到第六个槽中,所以局部变量表最大容量为6即可。

2、局部变量表中变量的槽号问题

看如下例子:

arduino 复制代码
public class Test {
    long add(int a,long b){
        long value = a + b;
        return value;
    }
}

javap打印如下:

这个例子的局部变量有this、a、b、value四个,为什么局部变量表最大容量比变量个数还多?为什么value的槽号是4(也就是第5个槽,因为槽号从0开始),槽号3去哪了?

因为b和value类型是long,上一小节说了,一个槽可以容纳一个 32 位的数据类型,而long是64位,所以需要占用两个槽的位置,如下图:

所以局部变量表最大容量是6,value存在第5、6两个槽中。除了long类型占两个槽之外,double类型也占两个槽

3、跳转指令和ASM的Label理解

1. if跳转

ini 复制代码
public class Test {
    public void jump(int a) {
        if (a == 18) {
            a = 10;
        } else {
            a = 20;
        }
    }
}

上面代码是一个简单的if判断,来分别看一下javap的字节码指令、ASM的字节码指令和ASM Core API之间的关联:

可以看到,将局部变量a以及常量18压入操作栈后,执行了if_icmpne指令,在javap的字节码中,if_icmpne后面的操作数是12,表示如果满足条件,跳转至指令12(对应于代码中else块中的语句),12之前的指令不再执行;如果不满足条件,则指令继续按顺序往下执行(对应于代码中if块中的语句),执行完if中的语句后,到指令9,会跳转至指令15,结束方法。

而在ASM中,由于是人为编写字节码文件,所以很难提前得知后面指令码的序号(比如一个文档的目录都是在文档写完后才创建,无法提前创建目录,因为只有文档写完后才能知道每个章节在第几页),也就无法像javap字节码中那样直接跳转至指令12,但是我们又需要知道要跳转到哪里,所以就引入了Label的概念。上图中IF_ICMPNE L0就表示跳转至L0处,L0就是一个Label。

在编写ASM代码时,需要用到Label的时候就new一个出来,然后在后面要跳转到的地方调用visitLabel方法插入label,这样就会跳转到label这里,继续往下执行后面的指令。注意Label不是jvm中的指令码,而是ASM中为了实现跳转功能而定义的,jvm中没有label这个东西。

2. switch跳转

switch跳转原理和if跳转一样,都是用Label,不过多赘述,这里就看一下如何写ASM代码。

arduino 复制代码
public class Test {
    public void jump(int a) {
        String str;
        switch (a){
            case 0:
                str = "0";
                break;
            case 1:
            case 2:
                str = "正数";
                break;
            default:
                str = "负数";
                break;
        }
    }
}

很简单的一个switch,javap看下字节码指令:

可以看到switch的字节码指令是tableswitch。对应ASM代码如下:

一眼就能看到visitTableSwitchInsn方法,源码中方法签名如下:

arduino 复制代码
public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels) {
  ...
}

解释一下参数含义:

min、max:switch的case的两个边界值,对应本例就是0和2

dflt:default要跳转到的Label

labels:一个Label类型的可变参数,对应每个case要跳转到的Label

这里可能有个疑惑,为什么可以直接设置case的两个边界值?它怎么知道我中间有多少个case?如果我的case是0,6,8三个,该怎么写?

实际上在字节码文件中,switch是分为两种的,一种就是本例中这种case值为连续的,还有一种则是case值不连续的。case值不连续的switch字节码指令是lookupswitch。使用了visitTableSwitchInsn方法就说明该switch是连续的,所以只需传入两个边界值即可。

来看下case值不连续的switch:

arduino 复制代码
public class Test {
    public void jump(int a) {
        String str;
        switch (a){
            case 0:
                str = "0";
                break;
            case 6:
            case 8:
                str = "正数";
                break;
            default:
                str = "负数";
                break;
        }
    }
}

javap后:

可以看到指令变成了lookupswitch。

在ASM中lookupswitch对应visitLookupSwitchInsn方法,方法签名为:

arduino 复制代码
public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
  ...
}

dflt:default要跳转到的Label

keys:所有case值的数组,对应本例就是[0,6,8]

labels:所有case值对应跳转到的label数组

需要注意的是,dflt不能传null,即使你的java代码switch没有写default分支,在编译成字节码文件的时候,jvm也会自动给加上default,default会直接跳转至return指令,所以dflt也要传入一个label。示例如下:

参考文章:

Java字节码指令详解,1 万字20 张图带你彻底掌握字节码指令 | 二哥的Java进阶之路

JVM 字节码 对照表

相关推荐
小猫猫猫◍˃ᵕ˂◍15 分钟前
备忘录模式:快速恢复原始数据
android·java·备忘录模式
liuyuzhongcc23 分钟前
List 接口中的 sort 和 forEach 方法
java·数据结构·python·list
五月茶27 分钟前
Spring MVC
java·spring·mvc
sjsjsbbsbsn36 分钟前
Spring Boot定时任务原理
java·spring boot·后端
yqcoder38 分钟前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
菜鸟蹦迪1 小时前
八股文实战之JUC:ArrayList不安全性
java
2501_903238651 小时前
Spring MVC配置与自定义的深度解析
java·spring·mvc·个人开发
逻各斯1 小时前
redis中的Lua脚本,redis的事务机制
java·redis·lua
计算机毕设指导61 小时前
基于Springboot学生宿舍水电信息管理系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·maven
计算机-秋大田1 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计