jvm8-字节码指令

简介

  • java虚拟机的指令是由一个字节长度的,代表着某种特定操作含义的数字,称之为操作码,以及跟随其后的零至多个代表此操作所需参数的操作数而构成
  • 操作码的长度为1个字节,因此最大只有256条
  • 基于栈的指令集架构
    (还有一种是基于寄存器的)

字节码与数据类型

i l f d a代表引用类型

加载和存储指令

  • 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
  • 将局部变量表加载到操作数栈:iload lload fload dload aload
  • 将一个数值从操作数栈存储到局部变量表:istore lfda
  • 将一个常量加载到操作数栈:bipush sipush ldc ldc_w ldc2_w aconst_null iconst_m1 iconst
  • 扩充局部变量表的访问索引的指令:wide
    例子:
public class Hello{
        private String str1 = "bbb";
        private static String str2 = "ccc";
        public static void main(){
                System.out.print("aaa");

        }
        public int add(int i, int j){
                return i + j;
        }
        public int add2(){
                return 1 + 1;
        }
}

args_size=3 其中一个隐藏的参数是this
stack=2 表示栈的最大深度为2
运算指令每次是将栈顶的两个数运算,所以要增大栈的深度,就要想办法多次进栈

add2()方法中的1+1 会直接编译成iconst_2,这是编译器的优化,能在编译器中确定的会在编译过程中确定,比如在代码中引用常量,编译后会把引用的地方替换成具体的值,所以有引用常量的地方如果有改变需要全部重新编译

指令的区别

运算指令

运算或算数指令用于对两个操作数栈上的值进行某种特定的运算,并把结果存储到操作数栈顶

  • 加法指令:add(int类型相加表示为iadd) i(int) l(long) f(float) d(double)
  • 减法指令:sub
  • 乘法指令:mul
  • 除法指令:div
  • 取余指令:rem
  • 取反指令:neg

类型转换指令

  • 类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作以及用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题
  • 宽化类型处理窄化类型处理
    宽化类型处理:例如
#1 User user = getUser();
#2 Object obj = user;
#3 User u = (User)obj;

2属于宽化类型处理

3属于窄化类型处理

类型转换例子

public class Demo {
    public static void main(String[] args) {
        int i = 24;
        long j = i * 60 * 60 * 1000;
        long k = i * 60 * 60 * 1000 * 1000;
        System.out.println(k/j);
    }
}

上面的例子输出并不是1000,原因就是指令执行顺序是先用int类型计算得出结果为int,然后再将int转化为long,计算k时溢出了

int类型溢出的计算方法为:

k = k + Integer.MIN_VALUE - Integer.MAX_VALUE - 1;
  • l2b i2c i2s l2i...

对象创建与访问指令

  • 创建类实例的指令:new
  • 创建数组的指令:newarray(创建数组) anewarray(创建应用类型数组) multianewarray(多维数组)
  • 访问类字段getfield putfield getstatic putstatic
  • 把数组元素加载到操作数栈的指令:baload
  • 将操作数栈的值存储到数组元素:astore
  • 取数组长度的指令:arraylength
  • 检查实例类型的指令:instanceof checkcast

操作数栈管理指令

  • 操作数栈指令用于直接操作操作数栈
  • 将操作数栈的一个或者两个元素出栈:pop pop2
  • 复制栈顶一个或两个数值并将复制或者双份复制值重新压入栈顶:dup dup2 dup_x1 dup_x2
  • 将栈顶的两个数值替换:swap

控制转移指令 (if else switch for while)

  • 控制转移指令可以让java虚拟机有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。可以认为控制转移指令就是在修改pc寄存器的值
  • 条件分支:ifeq iflt ifle ifne ifgt ifnull ifcomple...
  • 复合条件分支:tableswitch lookupswitch
  • 无条件分支:goto goto_w jsr jsr_w ret

方法调用指令

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是java语言中最常见的方法分派
  • invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
  • invokestatic指令用于调用类方法(static方法)

方法返回指令

  • 方法的调用指令与数据类型无关,而方法返回指令则是根据返回值的类型区分的。
  • 包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、- ireturn、freturn、dreturn和areturn
  • 另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的累初始化方法使用

异常处理指令

在程序中显式抛出异常的操作会由athrow指令实现,除了这种情况,还有别的异常会在其他java虚拟机指令检测到异常状况时由虚拟机自动抛出

早期的异常是通过字节码指令来处理的,所以会很耗费性能。现在使用的是Exception table来处理,几乎对性能没有影响

如果是正常情况,跟不使用try catch没有什么区别

同步指令

  • java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。(sychornized)
  • 方法级的同步是隐式的,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method info structure)中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放
  • 同步一段指令集序列通常时由java语言中的synchronized块来表示的,java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与java虚拟机两者协作支持

假如程序正常结束则会执行14行退出monitor,假如出现异常就会通过goto到20行退出monitor

posted @ 2020-12-22 10:18  link_ed  阅读(180)  评论(0编辑  收藏  举报