文章目录
背景
什么是字节码文件?
源代码经过编译器编译之后生成的一种二进制文件。它的内容是JVM指令,不像C、C++一样由编译器直接生成机器码。
什么是字节码指令?
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。
字节码指令主要是操作虚拟机栈的栈帧中的数据。
怎么查看字节码文件?
- idea中安装Jclasslib插件,选择顶部菜单栏的 View->Show Bytecode With Jclasslib 选项,即可查看当前类的字节码文件。
-
下载Jclasslib客户端,选择它用来打开class文件。
-
我们可以用javap(jdk自带的反解析工具)来查看一个class文件的字节码指令文件:
javap -verbose Main.class
eg.
public com.example.demo.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/demo/Main;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: sipush 129
8: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
11: astore_2
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: new #4 // class java/lang/StringBuilder
18: dup
19: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
22: aload_1
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
26: ldc #7 // String ,
28: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: aload_2
32: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
35: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
38: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
41: return
LineNumberTable:
line 9: 0
line 10: 5
line 11: 12
line 12: 41
LocalVariableTable:
Start Length Slot Name Signature
0 42 0 args [Ljava/lang/String;
5 37 1 a Ljava/lang/Integer;
12 30 2 b Ljava/lang/Integer;
字节码指令分析
1. 加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
指令 | 含义 | 示例 |
---|---|---|
load | 将一个局部变量加载到操作数栈 | iload 1:将int型本地变量1加载到操作数栈顶中 |
store | 将一个数值从操作数栈存储到局部变量表 | astore_0( 0-3 ):将操作数栈顶引用型(reference)数值存入到第一个局部变量中 |
push | 将一个整形数字(长度比较小)送到到栈顶 | bipush:将单字节的常量值(-128至127)推送至栈顶;sipush:将一个短整型常量值(-32768至32767)推送至栈顶 |
ldc | 将数值常量或String常量值从常量池中推送至栈顶 | ldc: 将int, float或String型常量值从常量池中推送至栈顶;ldc_w: 将int, float或String型常量值从常量池中推送至栈顶(宽索引);ldc2_w: 将long或double型常量值从常量池中推送至栈顶(宽索引) |
const | 把简单的数值类型( iconst_(0 - 5) )送到栈顶,不带参数 | aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>;iconst_m1:将int型的-1推送至操作数栈顶;fconst_1:将float型的1推送至操作数栈顶 |
wide | 扩充局部变量表的访问索引 |
2. 运算指令
用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶
指令 | 含义 | 示例 |
---|---|---|
add | 加法 | 支持 i, l, f, d;iadd:将栈顶两int型数值相加并将结果压入栈顶 |
sub | 减法 | 支持 i, l, f, d |
mul | 乘法 | 支持 i, l, f, d |
div | 除法 | 支持 i, l, f, d |
rem | 求余 | 支持 i, l, f, d |
neg | 取反 | 支持 i, l, f, d |
sh | 位移 | 支持 i, l;ishl:将int型数值(有符号)左移位指定位数并将结果压入栈顶;iushr:将int型数值(无符号)右移位指定位数并将结果压入栈顶 |
or | 按位或 | 支持 i, l;ior:将栈顶两int型数值作“按位或”并将结果压入栈顶 |
and | 按位与 | 支持 i, l; |
xor | 按位异或 | 支持 i, l; |
iinc | 局部变量自增 | |
cmp | 比较 | 支持 d, f, l;dcmpg、dcmpl、fcmpg、fcmpl、lcmp |
3. 类型转换指令
将两种不同的数值类型进行相互转换。
java虚拟机支持从小范围到大范围(如int到long/float/double)的直接转换,无需显式的转换指令,当然你也可以显式写出来;而从大范围到小范围的转换必须要显式的使用转换指令来完成。
指令 | 含义 | 示例 |
---|---|---|
i2l、f2b、l2f、l2d、f2d | 宽化类型转换 | |
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f | 窄化类型转换 |
4. 对象创建与访问指令
创建对象和创建数组,以及访问对象和访问数组的指令
指令 | 含义 | 示例 |
---|---|---|
new | 新建类 | |
newarray | 新建数组 | |
getfield、putfield、getstatic、putstatic | 访问类变量,静态变量 | |
aload | 将一个数组元素加载到操作数栈 | 支持 b, c, i, l, f, d;iaload:将int型数组的指定索引的元素加载到操作数栈顶中 |
astore | 将一个操作数栈顶的值存储到数组元素指定索引中 | 支持 b, c, i, l, f, d;dastore:将操作数栈顶中double型数值存入到指定数组的指定索引位置 |
arraylength | 取数组长度 | |
instanceof、checkcast | 检查实例类型 |
5. 操作数栈管理指令
直接操作操作数栈的指令
指令 | 含义 | 示例 |
---|---|---|
pop、pop2 | 将操作数栈顶1或2个元素出栈 | |
dup、dup2、dup_x1、dup_x2、dup2_x1、dup2_x2 | 复制1或2份操作数栈顶1或2个元素,重新压入栈顶 | |
swap | 将操作数栈最顶端两个元素互换 |
6. 控制转移指令
可以让java虚拟机从 指定位置 的指令的下一条指令继续执行程序,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。
指令 | 含义 | 示例 |
---|---|---|
ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne | 条件分支 | |
tableswitch、lookupswitch | 复合条件分支 | |
goto、goto_w、jsr、jsr_w、ret | 无条件分支 |
7. 方法调用和返回指令
指令 | 含义 | 示例 |
---|---|---|
invokevirtual | 调用对象实例,根据对象的实际类型进行分派(虚方法分派) | |
invokeinterface | 调用接口方法,它对在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用 | |
invokespecial | 调用一些需要特殊处理的实例方法,如实例初始化方法、私有方法、父类方法 | |
invokestatic | 调用类方法(static方法) | |
invokedynamic | 执行在运行时动态解析出调用点限定符所引用的方法,invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的 |
8. 异常处理指令
指令 | 含义 | 示例 |
---|---|---|
athrow | 程序中显式抛出异常(throw语句) |
9. 同步指令
java虚拟机支持方法级的同步和方法内部代码块的同步,这两种同步结构都是使用管程(Mentor)来支持的。
方法级的同步是隐式的,即无须通过字节码指令来控制。它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
方法内部代码块的同步需要显式声明指令,来支持synchronized关键字语义
指令 | 含义 | 示例 |
---|---|---|
monitorenter、monitorexit | 方法内部代码块的同步,调用过的每个monitorenter指令必须要执行其对应的monitorexit指令 |
相关:https://blog.csdn.net/YABAJ/article/details/89240707