字节码是由操作码opcode和操作数operand组成的。操作码的长度是一个字节-128到127
指令执行的伪代码如下:
do{
自动计算pc寄存器;
从PC寄存器的位置取出操作码;
if(存在操作数) 取出操作数;
执行操作码定义的操作
}while(处理下一次循环)
指令分类:
加载和存储指令
算术指令
类型转换指令
对象的创建和操作
操作数栈管理指令
控制转移指令
方法调用和返回指令
抛出异常
同步
大多数指令都不直接支持byte,short,char和boolean,处理时都说转化为int类型
格式为 _load_
如iload_<2>。表示加载第二个变量,iload_<3>表示加载第三个变量 iload 5表示加载第五个变量
简写和对应具体关系如下:
i : int
l : long
f : float
d : double
c : char
b : byte
a : reference(引用)
本地变量加载到操作数栈指令:
iload,iload_
这里没有byte和char,iload和ilad_0是一样的。其他类似的带n的省略
从操作数栈存储到局部变量表指令:
istore,istore_
类似的省略
常量加载到操作数栈
bipush,sipush,ldc,ldc_w,ldc2_w,aonst_null,iconst_ml const_ lconst_
上面带n的是一类指令,如iload_
加载和存储是一个互逆的过程,加载是从变量到操作数栈,存储是从操作数栈到变量
算术指令是对两个操作数栈上的值进行某种运算,并将结果重新压入栈
大体分为:对整数计算和对浮点数计算
算术指令 | |
指令名 | 指令内容 |
加法指令 | iadd ladd dadd fadd |
减法指令 | sub lsub dsub fsub |
乘法指令 |
imul lmul dmul fmul |
除法指令 | idiv ldiv ddiv fdiv |
求余指令 | irem lrem frem drem |
求负值指令 | ineg dneg lneg fneg |
移位指令 | ishl ishr ushr lshl lshr lushr |
按位或指令 | ior lor |
按位与指令 | iand land |
按位异或 | ixor lxor |
局部变量自增指令 | iinc |
比较指令 | dcmpg dcmpl fcmpg fcmpl lcmp |
虚拟机没有明确规定整形数据溢出的情况。如打印1<<31只会打印负值,但是除以0会抛异常,浮点数除以0不会抛异常,但是会有抛出一个字符串
用于开发人员代码中的显示类型转换或者解决虚拟机不完备的问题(byte,char和boolean的类型转换成int)
Java虚拟机规范SE8中原文提到,类型转换指令其实并不存在,因为byte char和boolean本来就是按int存储的所以不存在转化,觉得这一段与一开始说的类型转化用于虚拟机不完备的情况,有点冲突。
类型转换有两种:宽化与窄化。
宽化是int转long,转double这种小数据向大数据转化,窄化相反。
指令为i2l,i2f,i2d,l2f,l2d,f2d,2表示to 宽化转换通常不会丢失精度
这里因为long占的空间比float要长,不明白为什么是宽化转换,跑了一下,可能是浮点数表示形式允许超过long的范围
窄化就是l2i等,通常会丢失精度,丢失部分数值,但是不会抛出异常
创建类实例 | new |
创建数组 | newarray anewarray multianewayya |
访问类字段(静态字段)和类实例字段(非静态字段) | getfield putfield getstatic putstatic |
将数组加载到操作数栈 | baload caload saload iaload laload faload daload aaload |
将一个操作数栈的值存储到数组中 | 上面的load换成store |
取数组长度指令 | arraylength |
检查类实例或者数组类型指令 | instanceof checkcast |
pop,pop2,dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2 swap
栈顶的一个或两个元素出栈(两个刚好用于计算我觉得) | pop pop2 |
复制栈顶一个或两个数值并将复制值或双份的复制值重新亚茹栈顶 | dup dup2 dup_x1 dup_x2 dup2_x1 dup2_x2 |
将栈顶两个元素互换 | swap |
条件分支 | ifeq ifne iflt ifle ifgt ifge ifnull ifnonull if_icmpeq if_icmpne if_icmplt if_icmpgt if_icmpge if_acmpeq if_acmpne |
复合条件分支 | tableswitch lookupswitch |
无条件分支 | goto goto_w jsr jsr_w ret |
boolean byte char short都使用int类型的比较指令来完成,而对于long flaot double则先执行比较指令,返回一个整数数值到操作数栈中,随后执行int的条件分支完成条件分支
所有的比较最终都会转化为int的比较
invokevirtual指令用于调用对象的实例方法
invokeinterface用于调用接口,在运行时搜索特定对象实现这个接口的方法并调用
invokespecial用于处理一些特殊实例方法,如初始化方法,私有方法和父类方法
invokestatic调用类方法(静态方法)
invokedynamic用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面四条的指令分派固化在java虚拟机内部,而这一条是由用户的引导方法决定的
方法调用指令和数据类型无关,但是返回是数据相关的,如ireturn返回Boolean,char,char,short,int的值,还有lreturn,freturn,dreturn,areturn
还有一条用于返回void,实例化方法和初始化方法使用的return
由athrow实现,虚拟机规定了一些运行时异常在jvm检测到异常时自动抛出
jvm可以支持方法级的同步和方法内部的一段指令序列同步,这两种同步结构都是用同步锁(monitor)来实现的。
方法级的同步锁是隐式的,即无需通过字节码指令控制。
可以在方法常量池的方法表结构中的ACC_SYNCHRONNIZED查看一个方法是否同步
当调用方法时,调用指令会先查看是否有同步标记,如果有会让执行线程先持有同步锁,然后执行方法,执行完成后释放锁。
如果一个同步方法遇到异常且方法未对异常做处理,那么会在异常抛出的时候释放锁。
指令序列的同步通常用来表示java的synchronized块,jvm的指令集中用monitorenter和monitorexit来支持这个关键字,一个进入锁一个退出锁。
结构化锁定是指在方法调用期间每一个同步锁退出都与前面同步锁进入的情形相同,因为无法保证所有提交给jvm执行的代码都满足结构化锁定,因此jvm允许通过以下两条规则使结构化锁定成立
假设T是线程,M表示同步锁
1、T在方法执行时持有同步锁M的次数必须与T在方法执行时释放同步锁的次数相等
2、在方法调用过程中,任何时刻都不会出现线程T释放同步锁M的次数比T持有同步锁M次数多的情况
对于指令的查看可以使用 javap-c
public static void main(java.lang.String[]);
Code:
0: iconst_1 // 将常量加载入栈(数值是1-5的时候用iconst)
1: istore_1 // 读取栈的数据存储到变量
2: bipush 10 // 数值是-128~127的时候用bipush 压入栈 如果arraylength
// 是5的话这里就是iconst_5
4: newarray int // 创建一个数组
6: astore_2 // 将栈中的引用存入到变量中
7: return // 返回
1、前面说的一类 _load_
2、超过一定个数就使用类似bipush这种指令了
3、javap 的第二列是表示操作码对应的操作数类型