- 1、通过javap命令可以查看一个java类反汇编得到的Class文件版本号、常量池、访问标识、变量表、指令代码行号表等等信息。不显示类索引、父类索引、接口索引集合、( )、()等结构
- 2、通过对前面例子代码反汇编文件的简单分析,可以发现,一个方法的执行通常会涉及下面几块内存的操作
- (1)java栈中:局部变量表、操作数栈。
- (2)java堆:通过对象的地址引用去操作。
- (3)常量池。
- (4)其他如帧数据区、方法区的剩余部分等情况,测试中没有显示出来,这里说明一下。
- 3、平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行的,可以参考官方文档查看每个指令的含义,很简单:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
指令分类
由于完全介绍和学习这些指令需要花费大量时间,这里将JVN中的字节码指令集按用途大致分成9类。
- 1、加载与存储指令
- 2、算术指令
- 3、类型转换指令
- 4、对象的创建与访问指令
- 5、方法调用与返回指令
- 6、操作数栈管理指令
- 7、比较控制指令
- 8、异常处理指令
- 9、同步控制指令
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 。
1、加载与存储
加载
- 指令const系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有: iconst_i(i从-1到5)、lconst_l>(1从0到1)、fconst_f(f从0到2)、dconst_d (d从0到1)、aconst_null。比如:
- iconst_m1将-1压入操作数栈;
- iconst_x(x为0到5)将x压入栈:
- lconst_0、lconst_1分别将长整数0和1压入栈;
- fconst_0、fconst_1、fconst_2分别将浮点数0、1、2压入栈;
- dconst_0和dconst_1分别将double型0和1压入栈。
- aconst_null将null压入操作数栈;
从指令的命名上不难找出规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,1表示长整数,f表示浮点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出。
_后面数字2为局部变量表中索引。
-
指令push系列:主要包括bipush和sipush。它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈。
-
指令ldc系列:如果以上指令都不能满足需求,那么可以使用万能的1dc指令,它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的内容压入堆栈。
指令总结:
存储
出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。
这类指令主要以store的形式存在,比如xstore,(x为i、l、f、d、a)、 xstore_n(x 为i、l、f、d、a, n 为1至3)。
- 指令istore_n将从操作数栈中弹出一个整数,并把它赋值给局部变量索引n位置。
- 指令xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置。
2、算数指令
算术指令
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem(remainder:余数)
- 取反指令:ineg、lneg、fneg、dneg(negation:取反)
- 自增指令:iinc
位运算指令,又可分为:
-
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
-
按位或指令:ior、lor
-
按位与指令:iand、land
-
按位异或指令:ixor、lxor
比较指令
比较指令:dcmpg、dcmlp、fcmpg、fcmpl、lcmp
3、类型转换
宽化类型转换
小范围类型向大范围类型的安全转换
-
从 int 类型到 long、float 或者 double 类型,对应的指令为:i2l、i2f、i2d
-
从 long 类型到 float、double 类型。对应的指令为:l2f、l2d
-
从 flaot 类型到 double 类型。对应的指令为:f2d
简化为:int --> long --> float --> double
窄化类型转换
- 从 int 类型至 byte、short 或者 char 类型。对应的指令有:i2b、i2c、i2s
- 从 long 类型到 int 类型。对应的指令有:l2i
- 从 float 类型到 int 或者 long 类型。对应的指令有:f2i、f2l
- 从 double 类型到 int、long 或者float 类型。对应的指令有:d2i、d2l、d2f
通过向最接近数舍入模式舍入一个可以使用 float 类型表示的数字。最后结果根据下面这 3 条规则判断:
- 如果转换结果的绝对值太小而无法使用 float 来表示,将返回 float 类 型的正负零
- 如果转换结果的绝对值太大而无法使用 float 来表示,将返回 float 类 型的正负无穷大
- 对于 double 类型的 NaN 值将按规定转换为 float 类型的 NaN
无论是宽化还是窄化类型转换都存在进度丢失的问题。
4、对象的创建与访问指令
创建对象指令
创建类实例的指令:new 它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后, 将对象的引用压入栈
创建数组的指令:newarray、anewarray、multianewarray
- newarray:创建基本类型数组
- anewarray:创建引用类型数组
- multianewarray:创建多维数组
字段访问指令
- 访问类字段(static 字段,或者称为类变量)的指令:getstatic、putstatic
- 访问类实例字段(非 static 字段,或者称为实例变量)的指令:getfield、putfield
数组操作指令
数组操作指令主要有:xastore 和 xaload 指令。具体为:
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、 laload、faload、daload、aaload
- 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、 iastore、lastore、fastore、dastore、aastore
取数组长度的指令:arraylength 该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈
- xaload:表示将数组的元素压栈,比如 saload、caload 分别表示压入 short 数组和 char 数组。指令 xaload 在执行时,要求操作数中栈顶元素为 数组索引 i,栈顶顺位第 2 个元素为数组引用 a,该指令会弹出栈顶这两个 元素,并将 a[i] 重新压入堆栈
- xastore:则专门针对数组操作,以 iastore 为例,它用于给一个 int 数组的给 定索引赋值。在 iastore 执行前,操作数栈顶需要以此准备 3 个元素:值、 索引、数组饮用,iastore 会弹出这 3 个值,并将值赋给数组中指定索引的位 置
类型检查指令
检查类实例或数组类型的指令:instanceof、checkcast
- checkcast:用于检查类型强制转换是否可以进行。如果可以进行,那么 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常
- instanceof :用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈
5、方法调用与返回指令
方法调用
- invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派 (虚方法分派),支持多态。这也是 Java 语言中最常见的方法分派方式
- invokeinterface 指令用于调用接口方法,它会在运行时搜索由特定对象所实 现的这个接口方法,并找出适合的方法进行调用
- invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化 方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会 在调用时进行动态派发
- invokestatic 指令用于调用命名类中的类方法(static 方法)。这是静态绑定的
- invokedynamic 调用动态绑定的方法,这个是 JDK 1.7 后新加入的指令。用 于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令 的分派逻辑是由用户所设定的引导方法决定的
方法返回
- ireturn:返回值是 boolean、byte、char、short 和 int 类型时使用
- lreturn:long类型
- freturn:float类型
- dreturn :dobble类型
- areturn :返回引用类型
- return指令:供声明为 void 的方法、实例初始化方法以及 类和接口的类初始化方法使用
6、操作数栈管理指令
-
将一个或两个元素从栈顶弹出,直接废弃:pop、pop2
- pop:将栈顶的 1 个 Slot 数值出栈。例如 1 个 short 类型数值
- pop2:将栈顶的 2 个 Slot 数
-
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、 dup2、dup_x1、dup2_x1、du p_x2、dup2_x2
- 不带 _x 的指令是复制栈顶数据并压入栈顶。包括两个指令,dup 和 dup2(dup 的系数代表要复制的 Slot 个数 )
- dup 开头的指令用于复制 1 个 Slot 的数据。例如 1 个 int 或 1 个 reference 类型数据
- dup2 开头的指令用于复制 2 个 Slot 的数据。例如 1 个 long,或 2 个 int, 或 1 个 int 加 1 个 float 类型数据
- 带 _x 的指令:复制栈顶数据并插入栈顶以下的某个位置。共有 4 个指令:dup_x1、dup2_x1、dup_x2、dup2_x2。对于带 _x 的复制插入指令,只要将 指令的 dup 和 x 的系数相加,结果即为需要插入的位置。如:
- dup_x1 插入位置:1+1=2,即栈顶 2 个 Slot 下面
- dup_x2 插入位置:1+2=3,即栈顶 3 个 Slot 下面
- dup2_x1 插入位置:2+1=3,即栈顶 3 个 Slot 下面
- dup2_x2 插入位置:2+2=4,即栈顶 4 个 Slot 下面
-
将栈最顶端的两个 Slot 数值位置交换:swap
- Java 虚拟机没有提供交换两 个 64 位数据类型(long、double)数值的指令
-
nop 是一个非常特殊的指令,它的字节码为 0x00。和汇编语言中的 nop 一样,它表示什么都不做,这条指令一般可用于调试、占位等
这些指令属于通用型,对栈的压入或者弹出无需知名数据类型
7、控制转移指令
条件跳转指令
指令有:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull
统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条 件,则跳转到给定位置
比较条件跳转指令
指令有:if_icmped、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、 if_acmped 和 if_acmpne
- 其中指令助记符加上 “if_” 后,以字符 “i” 开头的指令针对 int 型整数操作 (也包括 short 和 byte 类型),以字符 “a” 开头的指令表示对象引用的比
多条件分支跳转
专为 switch-case 语句设计的,主要有:tableswitch 和 lookupswitch
- tableswitch 要求多个条件分支值是连续的,它内部只存放起始值和终止值, 以及若干个跳转偏移量,通过给定的操作数 index,可以立即定位到跳转偏 移量位置,因此效率比较高
- lookupswitch 内部存放着各个离散的 case-offset 对,每次执行都要搜索全部28 的 case-offset 对,找到匹配的 case 值,并根据对应的 offset 计算跳转地址, 因此效率较低
无条件跳转
- 目前主要的无条件跳转指令:goto
指令 goto 接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到 偏移量给定的位置处 如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令 goto_w,它和 goto 有相同的作用,但是它接收 4 个字节的操作数,可以表示更 大的地址范围 指令 jsr、jsr_w、ret 虽然也是无条件跳转的,但主要用于 try-finally 语句, 且已经被虚拟机逐渐废弃。
8、异常处理指令
- 指令:athrow
显式抛出异常的操作(throw 语句)都是由 athrow 指令来实现的 除了使用 throw 语句显式抛出异常情况之外,JVM 规范还规定了许多运行 时一场会在其它 Java 虚拟机指令检测到异常状况时自动抛出。
异常处理与异常表
处理异常(catch 语句)不是由字节码指令来实现的(早期 使用 jsr、ret 指令),而是采用异常表来完成的
异常表
如果一个方法定义了一个 try-catch 或者 try-finally 的异常处理,就会创建 一个异常表。它包含了每个异常处理或者 finally 块的信息。异常表保存了每个 异常处理信息。
- 起始位置
- 结束位置
- 程序计数器记录的代码处理的偏移地址
- 被捕获的异常类在常量池中的索引
方法结束后没有抛出异常,仍然执行 finally 块,在 return 前,直接跳到 finally块相关指令执行。
9、同步控制指令
方法级的同步
从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法
方法内指定指令序列的同步
指令集有 monitorenter 和 monitorexit
-
monitorenter :请求进入。查看当前对象的监视器计数器:
- 0 =》准许进入。
- 1 =》则判断持有当前监视器的线程是否为自己,
- 是 =》进入,
- 否 =》进行等待。对象的监视器计数器为 0,才会被允许进入同步
-
monitorexit :线程退出同步块时,声明退出
为保证方法在同步代码块中发生异常时 monitorenter 和 monitorexit 指令依然可以正确 配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,异常会执行执行 monitorexit 相关指令索引。