目录
字节码
java字节码由单字节指令组成,支持256个操作码,目前使用了200左右的操作码;
字节码指令分类
操作数栈相关指令,局部变量数组与操作数栈交互数据的指令;
对象操作指令,方法调用指令;
运算指令;
流程控制指令;
方法栈和栈帧(操作数栈,局部变量数组)
Java线程有线程独享的内存空间,即线程栈,线程内部方法调用时,会给每个被调用的方法
生成独立的内存空间,即栈帧;
栈帧中包含操作数栈,局部变量数组,类引用;
1,操作数栈中存放的是算术运算指令直接操作的数据;
2,局部变量数组中依次存放this指针(非静态方法),所传入的参数,字节码中的局部变
量。long类型以及double类型的值占两个单元,其余类型占一个单元。
3,类引用指向当前方法在运行时常量池中对应的class;
字节码指令
操作数栈相关指令
dup:复制栈顶元素(非long、非double类型的值),常用于复制new指令所生成的没被初始
化的引用;
pop:舍弃栈顶元素(非long、非double类型的值);
dup2:复制栈顶元素(long、double类型的值);
pop2:舍弃栈顶元素(long、double类型的值);
long和double类型,需要占据两个栈单元。
swap:交换栈顶两个元素的值。
例子:
public void test() {
Object o = new Object();
}
public test()V
L0
LINENUMBER 4 L0
NEW java/lang/Object
DUP // 复制操作数栈栈顶new出来的object引用
INVOKESPECIAL java/lang/Object.<init> ()V //调用栈顶元素的init方法,消耗栈顶元素
ASTORE 1
局部变量数组相关的指令
局部变量数组中的数据,大部分情况下需要加载到操作数栈里面,才能进行运算;
例外:iinc M N(M 为非负整数,N 为整数):iinc指令能直接作用于局部变量数组,将局部
变量数组的第 M 个单元中的 int 值增加 N,常用于 for 循环中自增量的更新。
加载:数据从局部变量数组加载到操作数栈;
存储:数据从操作数栈存储到局部变量数组;
局部变量数组的加载、存储指令需要指明所加载单元的下标。
Tips:操作数栈的压入弹出都是单条指令完成的。但是抛异常时,Java 虚拟机会清除操作数
栈上的所有内容,而后将异常实例压入操作数栈上。
将常量加载到操作数栈上的指令
例子:
public void test2() {
int i = 5;
}
public test2()V
L0
LINENUMBER 8 L0
ICONST_5 //把常量5赋值到操作数栈
ISTORE 1 //把操作数栈中栈顶元素(5),存储到局部变量数组的1号位置;0号位置放得是this;
高层语义的字节码指令
new:后面跟目标类,生成该类的未初始化的对象;
instanceof:后面跟目标类,判断栈顶元素是否为目标类 / 接口的实例。是则压入1,否则压
入0;
checkcast:后面跟目标类,判断栈顶元素是否为目标类 / 接口的实例。如果不是便抛出异
常;
athrow:将栈顶异常抛出;
monitorenter:为栈顶对象加锁;
monitorexit:为栈顶对象解锁;
字段访问指令
getstatic、putstatic:静态字段访问指令;
getfield、putfield:实例字段访问指令;
方法调用指令
invokestatic:调用静态方法;
invokespecial:调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或
构造器,和所实现接口的默认方法;
invokevirtual:调用非私有实例方法;
invokeinterface:调用接口方法;
invokedynamic:调用动态方法;jdk7之后的概念,调用“调用点”,“调用点”可链接至任意符合
条件的方法上,如方法句柄MethodHandle;jdk8之后的Lambda 表达式,基于该指令相关的机制
实现;
方法重载和重写
重载:同一个类中定义名字相同,参数类型不同的方法;Java编译器会根据所传入参数的声
明类型(不是实际类型)来选取重载方法。
重写:子类定义了与父类中非私有方法同名的方法,且这两个方法的参数类型相同。
如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法。
如果这两个方法都不是静态的,那么子类的方法重写了父类中的方法。
虚方法调用指令的动态绑定
重载方法的区分在编译阶段已经完成,可以认为Java 虚拟机不存在重载这一概念;
重写方法则需要在运行过程中根据调用者的动态类型来识别目标方法;
invokestatic,invokespecial指令调用的方法,Java 虚拟机能够直接识别具体的目标方法。
invokevirtual,invokeinterface指令调用的方法,大部分情况下,虚拟机需要在执行过程中,
根据调用者的动态类型,来确定具体的目标方法。例外:final修饰的方法,只会存在一个;
类加载的准备阶段做的工作包括:为静态字段分配内存,构造与该类相关联的方法表。(详
见JVM类加载篇)
方法表
方法表本质是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。
子类方法表中包含父类方法表中的所有方法;
子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。
动态绑定
invokevirtual,invokeinterface指令属于虚方法调用,虚方法调用指令中的符号引用,会在执
行之前解析成实际引用,实际引用可以理解为方法表的索引值。
方法执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的方法表中,根
据索引值获得目标方法。
数组访问指令表
newarray:新建基本类型数组;
anewarray:新建引用类型数组;
multianewarray:生成多维数组;
arraylength:求数组长度;
数据加载和存储指令,这些指令区分数据类型;
流程控制指令
goto:包括无条件跳转;
tableswitch:条件跳转指令,密集的cases;
lookupswtich:条件跳转指令,稀疏的cases;
返回指令,返回指令是区分类型的:
Tips:除返回指令外,其他流程控制指令附带一个或者多个字节码偏移量,代表需要跳转到的
位置。
查看字节码的方法
javap -c -verbose xxx //查看xxx类的class字节码信息;
javac -g xxx.java
编译时,加了-g参数会把额外的debug信息放进来,本地变量表;用javap查询时,信息会更详细;
idea等开发平台,可下载相应的工具;
字节码实例解读
代码:
public int test(int i) {
return ((i + 1) - 2) * 3 / 4;
}
字节码:
public test(I)I
L0
LINENUMBER 4 L0
ILOAD 1
ICONST_1
IADD
ICONST_2
ISUB
ICONST_3
IMUL
ICONST_4
IDIV
IRETURN
调用test(9)时,上述字节码运行过程: