精华内容
下载资源
问答
  • 字节码指令详解

    万次阅读 2020-08-05 16:49:04
    字节码指令详解 指令简介 在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程。 通常一条CPU指令包括两方面的内容:操作...

    字节码指令详解

    指令简介

    在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程。

    通常一条CPU指令包括两方面的内容:操作码和操作数,操作码表示要完成的操作,操作数表示参与运算的数据及其所在的单元地址(这个单元地址可以是寄存器、内存等)。

    Java虚拟机中的字节码指令与CPU中指令类似,Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。

    操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将大端排序存储,即高位在前的字节序。

    操作码只占一个字节,也就导致操作码个数不能超过256。

    class文件只会出现数字形式的操作码,但是为了便于人识别,操作码有他对应的助记符形式。

    对于基本数据类型,指令在设计的时候都用一个字母缩写来指代(boolean除外)。

    基本数据类型缩写
    byteb
    shorts
    inti
    longl
    floatf
    doubled
    charc
    refrencea
    boolean

    指令详解

    加载存储指令

    加载存储指令用来交换局部变量表和操作数栈中的数据,以及将常量加载到操作数栈。

    1. 将一个局部变量从局部变量表中加载到操作数栈栈顶:
    iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
    

    其中n为局部变量表中的slot的序号,double和long占用两个slot。

    1. 将一个操作数栈的栈顶元素存储到局部变量表:
    istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
    
    1. 将一个常量加载到操作数栈栈顶:
    bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
    

    操作数为将要操作的数值或者常量池行号。

    1. 扩充局部变量表的访问索引的指令:wide。

    运算指令

    运算指令会取出操作数栈栈顶的两个元素进行某种特定的运算,然后将结果重新存入到操作数栈栈顶。

    运算指令分为两种:整型运算的指令和浮点型运算的指令。

    无论是哪种运算指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,使用操作int类型的指令代替。

    加法指令:iadd、ladd、fadd、dadd

    减法指令:isub、lsub、fsub、dsub

    乘法指令:imul、lmul、fmul、dmul

    除法指令:idiv、ldiv、fdiv、ddiv

    求余指令:irem、lrem、frem、drem

    取反指令:ineg、lneg、fneg、dneg

    位移指令:ishl、ishr、iushr、lshl、lshr、lushr

    按位或指令:ior、lor

    按位与指令:iand、land

    按位异或指令:ixor、lxor

    局部变量自增指令:iinc

    比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

    类型转换指令

    类型转换指令可以将两种不同的数值类型进行相互转换。这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来解决字节码指令集不完备的问题。

    宽化指令:

    • int类型到long、float或者double类型
    • long类型到float、double类型
    • float类型到double类型

    处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

    对象的创建与存取指令

    new:创建对象。

    getfield:获取实例对象的属性的值。

    putfield:设置实例对象的属性的值。

    getstatic:获取类的静态属性的值。

    putstatic:设置类的静态属性的值。

    数组的创建与存取指令

    newarray:创建元素为基本数据类型的数组。

    anewarray:创建数据类型为引用类型的数组。

    multianewarray:创建多维数组。

    把一个数组元素加载到操作数栈栈顶的指令:baload、caload、saload、iaload、laload、faload、daload、aaload

    将一个操作数栈栈顶的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

    取数组长度的指令:arraylength

    检查类实例类型的指令

    instanceof、checkcast。

    操作数栈管理指令

    pop:将操作数栈的栈顶元素出栈。

    pop2:将操作数栈的栈顶两个元素出栈。

    复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2

    将栈最顶端的两个数值互换:swap

    控制转移指令

    控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。

    控制转移指令如下:

    • 条件分支: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。

    方法调用指令

    invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

    invokeinterface:指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

    invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

    invokestatic:指令用于调用类方法(static方法)。

    invokedynamic:指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

    方法调用指令与数据类型无关。

    方法返回指令

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

    异常处理指令

    在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现。

    同步指令

    monitorenter和monitorexit两条指令来支持synchronized关键字的语义。

    常见语法结构对应的字节码指令

    异常处理

    public class SynchronizedDemo {
    
        final Object lock = new Object();
    
        void doLock() {
            synchronized (lock) {
                System.out.println("lock");
            }
        }
    }
    

    对应的字节码如下:

    stack=2, locals=3, args_size=1
       0: aload_0
       1: getfield      #3                  // Field lock:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String lock
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any
    

    在synchronized生成的字节码中,其中包含两条monitorexit指令,是为了保证所有的异常条件,都能够退出。

    可以看到,编译后的字节码,带有一个叫Exception table的异常表,里面的每一行数据,都是一个异常处理:

    • from:指定字节码索引的开始位置
    • to:指定字节码索引的结束位置
    • targe:t异常处理的起始位置
    • type:异常类型

    也就是说,只要在from和to之间发生了type异常,就会跳转到target所指定的位置。

    装箱和拆箱

    public class BoxDemo {
    
        public Integer cal() {
            Integer a = 1000;
            int b = a * 10;
            return b;
        }
    }
    

    对应的字节码如下:

     0: sipush        1000
     3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     6: astore_1
     7: aload_1
     8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
    11: bipush        10
    13: imul
    14: istore_2
    15: iload_2
    16: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    19: areturn
    

    通过观察字节码,我们发现装箱和拆箱的本质:

    • 装箱:调用Xxx.valueOf()方法将基本数据类型包装成包装对象。
    • 拆箱:调用Xxx.xxxValue()方法将包装对象转为基本数据类型。

    数组

    package com.morris.jvm.bytecode;
    
    public class ArrayDemo {
        int getValue() {
            int[] arr = new int[]{1111, 2222, 3333, 4444};
            return arr[2];
        }
        int getLength(int[] arr) {
            return arr.length;
        }
    }
    
    

    getValue()方法对应的字节码如下:

    stack=4, locals=2, args_size=1
       0: iconst_4
       1: newarray       int
       3: dup
       4: iconst_0
       5: sipush        1111
       8: iastore
       9: dup
      10: iconst_1
      11: sipush        2222
      14: iastore
      15: dup
      16: iconst_2
      17: sipush        3333
      20: iastore
      21: dup
      22: iconst_3
      23: sipush        4444
      26: iastore
      27: astore_1
      28: aload_1
      29: iconst_2
      30: iaload
      31: ireturn
    LineNumberTable:
      line 5: 0
      line 6: 28
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      32     0  this   Lcom/morris/jvm/bytecode/ArrayDemo;
         28       4     1   arr   [I
    

    可以看到,新建数组的代码,被编译成了newarray指令。

    数组的创建具体操作:

    1. iconst_0:常量0压入操作数栈栈顶。
    2. sipush:将一个常量1111压入操作数栈栈顶。
    3. iastore:将栈顶int型数值存入数组的0索引位置。

    数组元素的访问,是通过第28~30行字节码来实现的:

    1. aload_1:从局部变量表中第二个引用类型的局部变量压入操作数栈栈顶,这个引用就是生成的数组的引用。
    2. iconst_2:将int型2压入操作数栈栈顶。
    3. iaload:将int型数组指定索引的值压入操作数栈栈顶。

    getLength()方法对应的字节码如下:

    0: aload_1
    1: arraylength
    2: ireturn
    

    获取数组的长度,是由字节码指令arraylength来完成的。

    更多精彩内容关注本人公众号:架构师升级之路
    在这里插入图片描述

    展开全文
  • 字节码文件及字节码指令

    千次阅读 2019-05-21 11:24:14
    本文主要内容是字节码文件的格式及字节码指令。 一、字节码文件 字节码是什么? 一开始我以为字节码是个非常复杂的东西,不修炼几千年根本无法了解其中的奥秘。但是剥丝抽茧后,我发现其实本质上就是一个特定...

    我记得开始学习Java的第一堂课时,我的大学老师是这样说的,Java号称是“一次编写,到处运行”,为什么有底气这样说,是因为Java程序并不是直接运行在操作系统上的,它通过不同操作系统上的Java虚拟机实现了“到处运行”的美好愿景。而且我的老师当时还说过,不止Java程序可以在Java虚拟机上运行,其他的程序也同样可以在Java虚拟机上运行。Java虚拟机并不认识具体的某种编程语言,而是编程语言要通过编译器编译成虚拟机认识的格式内容。那么虚拟机认识的格式内容就是本文主要讲述的一个神奇的东东–字节码文件。

    本文主要内容是字节码文件的格式及字节码指令。

    一、字节码文件

    字节码是什么?

    一开始我以为字节码是个非常复杂的东西,不修炼几千年根本无法了解其中的奥秘。但是剥丝抽茧后,我发现其实本质上就是一个特定格式排列各个字段信息的二进制流文件,用于虚拟机识别执行。根据Java虚拟机规范,字节码文件使用类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型,无符号数和表。

    下面首先来看一段为字节码而造的代码:

    package com.earl.se.basics;
    
    public class Demo5 implements Runnable {
    
        private int number = 1;
    
        @Override
        public void run() {
            System.out.println("Demo5 thread say hello byte code.");
        }
    
        public static void main(String[] args){
    
            System.out.println("Main thread say hello byte code.");
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    
                }
            }).start();
        }
    }
    
    

    当我通过javac命令编译了Demo5.java后,会生成字节码文件Demo5.class,通过十六进制编辑器WinHex将其打开,可以看到是如下的格式:

    在这里插入图片描述

    哈哈,仅仅是这么几行代码,生成的字节码文件看起来是不是都很头大,毕竟人脑还是适合阅读人类的文字而非机器所喜欢的文字。但是没办法,既然选择要正面刚虚拟机的知识,那么了解其内部的结构是必不可少的环节。接下来先介绍一下字节码文件中各个部分分别代表的意义,概念扫盲之后,其实是可以通过咱们开发常用的IDE来进行查看字节码文件的,这样是不是就显得轻松多了,嘿嘿~

    字节码文件结构

    字节码文件有两种数据格式:

    • 无符号数属于基本数据类型,以u1,u2,u4,u8分别表示1个字节,2个字节,4个字节,8个字节的无符号数。
    • 表是由多个无符号数或其他表作为数据项构成的复合数据类型。所有的表都是以“_info”结尾。

    下表是按照字节码文件中各字段排列顺序的全部格式:

    名称类型数量
    魔数(magic)u41
    子版本号(minor_version)u21
    主版本号(major_version)u21
    常量池计数值(constant_pool_count)u21
    常量池(constant_pool)cp_infoconstant_pool_count-1
    访问标志(access_flag)u21
    类索引(this_class)u21
    父类索引(super_class)u21
    接口计数值(interfaces_count)u21
    接口索引集合(interfaces)u2interface_count
    字段表集合计数器(fields_count)u21
    字段表集合(fields)field_infofields_count
    方法表集合计数器(methods_count)u21
    方法表集合(methods)method_infomethods_count
    属性表集合计数器(attributes_count)u21
    属性表集合(attributes)attribute_infoattributes_count

    下面依次来说明各个类型是什么含义:

    每个字节码文件开头的4个字节称为魔数,作用是确定这个字节码文件是否是一个能被虚拟机接受的字节码文件。在上图字节码文件中看到的开始的四个字节0xCAFEBABE代表的就是Java。

    接着魔数后的第5个和第6个两个字节表示子版本号,接下来的第7个和第8个字节表示主版本号。

    接下来是常量池相关信息。由于常量池中常量的数量是不固定的,因此在常量池入口处设置了一个常量池计数值。常量池中主要存放的是字面量和符号引用。
    字面量即如java语言中的常量概念。
    符号引用则属于编译原理方面的概念,包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
    在字节码文件中并不存储各个方法,字段的最终内存布局信息,因此当虚拟机运行时,需要从常量池中获取到符号引用,在类创建或运行时在找到具体的内存地址。

    接下来的两个字节代表访问标志,用于识别类或接口层次的访问信息,包括这个字节码文件对应的是类还是接口,是否定义为public类型,是否定义为abstract类型,类的话是否是final声明等。

    接下来是类索引,父类索引和接口索引集合。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类全限定名,接口索引集合描述这个类实现了哪些接口(如果这个类是接口,那么就是继承了哪些接口)。接口计数值表示这个类或接口实现或继承的接口数量,如果没有实现或继承接口,这个值为0。

    字段表集合计数器与字段表集合用来描述接口或类中声明的变量。

    方法表集合计数器与方法表集合用来描述接口或类中声明的方法。

    属性表集合计数器与属性表集合用来描述Class文件等在某些场景的专有信息。

    通过IDE来查看字节码文件

    扫盲结束,接下来讲讲如何通过我们常用的IDE来查看字节码文件。这里我选用的是IDEA,首先要安装字节码查看的插件。File->Settings->Plugins,在Marketplace中搜索jclasslib,然后安装jclasslib Bytecode viewer,重启IDEA即可完成安装。

    接下来就可以使用jclasslib来查看字节码文件了,还是以Demo5.java为例,在IDEA中打开这个文件,然后点击View->Show Bytecode With jclasslib,这样就会看到如下图的展示:

    在这里插入图片描述

    这样看起来是不是就清楚很多了,右侧的窗口中将字节码文件中的各个字段都直观地展示出来,再无需我们手动去将十六进制转换为人类文字了。

    二、字节码指令

    字节码指令是一个字节长度的,代表着某种特定操作含义的数字操作码,由操作码及代表此操作所需操作参数构成。主要包含以下的一些指令操作:

    1. 加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,主要包含以下的操作:
      • 将局部变量加载到操作栈,涉及指令有load指令
      • 将一个数值从操作数栈存储到局部变量表,涉及指令有store指令
      • 将一个常量加载到操作数栈,涉及指令有push指令,const指令
      • 扩充局部变量表的访问索引指令,wide指令
    2. 运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入操作栈顶。运算主要分为两种,对整型数据进行运算和对浮点型数据进行运算,主要包含以下操作:
      • 加法指令:add
      • 减法指令:sub
      • 乘法指令:mul
      • 除法指令:div
      • 求余指令:rem
      • 取反指令:neg
      • 位移指令:shl,shr,ushr
      • 按位或指令:or
      • 按位与指令:and
      • 按位异或指令:xor
      • 局部变量自增指令:inc
      • 比较指令:cmpg,cmpl,cmp
    3. 类型转换指令:用于将两种不同的数值类型进行相互转换,这些转换操作主要用于代码中的显式类型转换。
      • 对于小类型转大类型的操作,虚拟机直接支持,无需转换指令。
      • 对于大类型转小类型的操作,涉及指令有i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f。大转小的操作可能会导致精度丢失。
    4. 对象创建与访问指令,主要涉及以下操作:
      • 创建类实例的指令:new
      • 创建数组的指令:newarray,anewarray,multianewarray
      • 访问类字段和实例字段指令:getfield,getstatic,putfield,putstatic
      • 加载数组元素到操作数栈指令:aload
      • 将操作数栈的值存储到数组元素指令:astore
      • 取数组长度:arraylength
      • 检查类实例类型指令:instanceof,checkcast
    展开全文
  • JVM字节码指令集大全及其介绍

    千次阅读 多人点赞 2019-08-22 23:39:48
    JVM字节码指令介绍 字节码与数据类型 加载和存储指令 算术指令 类型转换指令 对象创建与访问指令 操作数栈管理指令 控制转移指令 方法调用和返回指令 异常处理指令 同步指令 JVM指令集大全 Java是怎么跨...

    本节将会着重介绍一下JVM中的指令集、Java是如何跨平台的、JVM指令集参考手册等内容。

    目录

    Java是怎么跨平台的

    平台无关的基石

    JVM字节码指令介绍

    字节码与数据类型

    加载和存储指令

    算术指令

    类型转换指令

    对象创建与访问指令

    操作数栈管理指令

    控制转移指令

    方法调用和返回指令

    异常处理指令

    同步指令

    JVM指令集大全


    Java是怎么跨平台的

    我们上计算机课的时候老师讲过:"计算机只能识别0和1,所以我们写的程序要经过编译器翻译成0和1组成的二进制格式计算机才能执行"。我们编译后产生的.class文件是二进制的字节码,字节码是不能被机器直接运行的,通过JVM把编译好的字节码转换成对应操作系统平台可以直接识别运行的机器码指令(二进制01代码),JVM充当了一个中间转换的桥梁,这样我们编写的Java文件就可以做到 "一次编译,到处运行" 。

    平台无关的基石

    各种不同平台的虚拟机与所有平台豆统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。Java虚拟机不和包括Java在内的任何语言绑定,它只与"Class 文件" 这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,其他的语言都可以用自己的编译器编译出能被Java虚拟机接受的有效的Class文件。其实现在很多语言都可以在Java虚拟机上运行了,比如kotlin、Scala、JRuby、Groovy、Jython。。。。。。

    JVM字节码指令介绍

    JVM官方文档JVM虚拟机规范对JDK8的指令集介绍地址为:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5,英语好的小伙伴可以看一看。

    Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码

    由于限制了Java虚拟机操作码的长度为一个字节(一个字节8位,2^{8} =256,即0~255),这意味着指令集的操作码总数不可能超过256条;又由于class文件格式放弃了编译后代码的操作数长度对齐,操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将会以big一endian顺序存储,即高位在前的字节序。例如,如果要将一个16位长度的无符号
    整数使用两个无符号字节存储起来(将它们命名为妙byte1和byte2),那这个16位无符号整数的值就是:(byte1<<8)|byte2。

    字节码指令流应当都是单字节对齐的,只有Iableswitch和lookupswitch两个指令例外,由于它们的操作数比较特殊,都是以4字节为界划分的,所以当这两个指令的参数位置不是4字节的倍数时,需要预留出相应的空位补全到4字节的倍数以实现对齐。

    这种操作在某种程度上会导致解释执行字节码时损失一些性能。但这样做的优势也非常明显,放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号;用一个字节来代表操作码,也是为了尽可能获得短小精于的编译代码。

    如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解:

    do {
           自动计算PC寄存器的值加1;
           根据PC寄存器的指示位置,从字节码流中取出操作码;
           if(字节码存在操作数) 从字节码流中取出操作数;
           执行操作码所定义的操作;
    } while(字节码流长度>0)

    字节码与数据类型

    在Java虚拟机的指令集中,大多数的指令都包含了其所操作的数据类型信息。例如,iload 指令用于从局部变量表中加载int 类型的数据到操作数栈中,而fload指令加载的则是 float 类型的数据。这两个指令的操作可能会是由同一段代码来实现的,但它们必须拥有各自独立的操作码。

    对于大部分与数据类型相关的字节码指令来说,它们的操作码助记符中的首字母都跟操作的数据类型相关:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符没有明确用字母指明数据类型(比如arraylength只能操作数组),还有些指令是与数据类型无关的(比如goto指令用于跳转)。

    因为Java虚拟机的操作码长度只有一个字节,所以不能每一种与数据类型相关的指令都支持Java虚拟机的所有运行时数据类型。因此,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令,有一些单独的指令可以再必要的时候用来将一些不支持的类型转换为可支持的类型。

    下面列出了Java虚拟机支持的字节码指令集。用数据类型列所代表的特殊字符替换opcode列的指令模板中的T,就可以得到一个具体的字节码指令(比如byte类型的b替换Tipush中的T后得到bipush)。如果在表中指令模板与数据类型两列共同确定的单元格为空,则说明虚拟机不支持对这种数据类型执行这项操作。

    我们可以发现大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(sign一extend)为相应的int类型数据,将boolean和char类型数据零位扩展(zero一extend)为相应的1nt类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,操作数的实际类型为boolean、byte、char及short的大多数操作,都可以用操作数的运算类型(computationaltype)为int的指令来完成。

     Java虚拟机中,实际类型与运算类型的对应关系如下表:

    下面介绍一下JVM中的各种类型的指令集,好多没有列出来,可以参看最后的JVM指令集大全参考手册。

    加载和存储指令

    加载和存储指令用于将数据从栈帧的本地变量表和操作数栈之间来回传递:

    • 将一个本地变量加载到操作数栈的指令::iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。这里load后面的<n>代表的是当前栈帧中局部变量表的索引值,执行load操作后会把位于索引n位置的数据入栈到操作数栈顶
    • 将一个数值从操作数栈存储到局部变量表的指令:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。这里store后面的<n>代表的是当前栈帧中局部变量表的索引值,执行store操作后会把操作数栈顶的数据出栈,然后保存到位于索引n位置的局部变量表中
    • 将一个常量加载到操作数栈的指令:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。const操作就是把对应类型的常量数据入栈到操作数栈的栈顶。例如iconst_10则表示把int类型的常量10入栈到操作数栈顶。
    • 扩充局部变量表的访问索引的指令:wide

    我们看到上面有很多指令都是 指令_<n>,比如iload_<n>其实是表示一组指令(iload_<0>,iload_<1>,iload_<2>,iload_<3>)。在尖括号之间的字面指定了隐含操作数的数据类型:<n>代表的是非负的整数,<i>代表的是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表double类型。操作byte,char和short类型的数据时,经常用int类型的指令来表示。

    如果是实例方法(非static的方法),那么局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用"this"。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot(比如方法method(int a1,inta2),参数表为a1和a2,则局部变量表索引0、1、2则分别存储了this指针、a1、a2,如果方法内部有其他内部变量,则在局部变量表中存在a2之后的位置)。 

    算术指令

    算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令。在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、short、char和boolean类型的算术指令。对于这些数据的运算,都使用int类型的衍令来处理:整型与浮点类型的算术指令在溢出和被零除的时候也有各自不同的行为表现。

    Java虚拟机的指令集直接支持了在Java语言规范中描述的各种对整型及浮点类型数进行操作的语义。Java虚拟机没有明确规定整型数据溢出(两个很大的整数相加,可能出现的结果是负数)的情况,只有整数除法指令(idiv和Idiv)及整数求余指令(irem和lrem)在除数为零时会导致虚拟机抛出异常。如果发生了这种情况,虚拟机将会抛出ArlthmeticException异常。

    虚拟机要求在进行浮点数运算时,所有的运算结果都必须舍入到适当的梢度。非精确的结果必须舍入为可表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,那将有些选择最低有效位为0的。这种舍入方式称为向最接近数舍入模式。

    类型转换指令

    类型转换指令可以将两种不同的数值类型进行相互转换。这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令的不完备的问题(上面说的byte、short、char和boolean)。

    Java虚拟机支持宽化类型转换(小范围类型向大范围类型的转换)、窄化类型转换(大范围类型向小范围类型的转换)两种:

    宽化类型转换

    • int类型到long、float或者double类型。
    • long类型到float、double类型。
    • float类型到double类型。

    类型转换指令有:i2l、i2f,i2d、l2f、l2d、f2d。"2"表示to的意思,比如i2l表示int转换成long。宽化类型转换是不会导致Java虚拟机抛出运行时异常的。

    窄化类型转换:

    • 从int类型到byte、short或者char类型

    • 从long类型到int类型

    • 从float类型到int或者long类型

    • 从double类型到int、long或者float类型

    窄化类型转换指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度。窄化类型转换是不会导致Java虚拟机抛出运行时异常的。

    对象创建与访问指令

    虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。

    操作数栈管理指令

    Java虚拟机提供了一些用于直接控制操作数栈的指令,包括:pop,pop2,dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2,swap。

    控制转移指令

    控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。

    在Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作,为了可以无须明显标识一个实体值是否null,也有专门的指令用来检测null值。

    boolean、byte、char和short类型的条件分支比较操作,都使用int类型的比较指令来完成,而对于long、float和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型数值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转。由于各种类型的比较最终都会转化为int类型的比较操作,所以基于int类型比较的重要性,Java虚拟机提供了非常丰富的int类型的条件分支指令。

    所有int类型的条件分支转移指令进行的都是有符号的比较操作。

    方法调用和返回指令

    • invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
    • invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
    • invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化(<init>)方法、私有方法和父类方法。
    • invokestatic  调用静态方法(static方法)。
    • invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

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

    异常处理指令

    在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在前面介绍的整数运算中,当除数为零时,虚拟机会在idiv或Idiv指令中抛出ArithmeticExceptton异常。

    而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成的。

    同步指令

    Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用同步锁(monitor)来支持的。

    方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法,当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有同步锁,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放同步锁。在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获取到同一个锁。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

    同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchromzed关键字需要Javac编译器与Java虚拟两者共同协作支持。

    package com.wkp.clone;
    
    public class TestLock {
    
    	public void onlyMe(Object f) {
    		synchronized (f) {
    			doSomething();
    		}
    	}
    
    	private void doSomething() {
    		System.out.println("执行方法");
    	}
    }
    

    上面代码通过 javap -c TestLock.class > TestLock.txt 将class文件进行反汇编,得到如下指令代码

    Compiled from "TestLock.java"
    public class com.wkp.clone.TestLock {
      public com.wkp.clone.TestLock();
        Code:
           0: aload_0
           1: invokespecial #8                  // Method java/lang/Object."<init>":()V
           4: return
    
      public void onlyMe(java.lang.Object);
        Code:
           0: aload_1				//将对象f推送至操作数栈顶
           1: dup					//复制栈顶元素(对象f的引用)
           2: astore_2				//将栈顶元素复制到本地变量表Slot 2(第三个变量)
           3: monitorenter			//以栈顶元素对象f作为锁,开始同步
           4: aload_0				//将局部变量Slot 0(this指针)的元素入栈
           5: invokespecial #16     //调用doSomething()方法
           8: aload_2				//将本地变量表Slot 2元素(f)入栈
           9: monitorexit			//释放锁退出同步
          10: goto          16		//方法正常返回,跳转到16
          13: aload_2				//将本地变量表Slot 2元素(f)入栈
          14: monitorexit			//退出同步
          15: athrow				//将栈顶的异常对象抛给onlyMe的调用者
          16: return				//方法返回
        Exception table:
           from    to  target type
               4    10    13   any
              13    15    13   any
    }
    

    编译器必须确保无论方法通过何种方式完成,方法中调用过的每条momtor指令都必须执行其对应的momtorexlt指令,而无论这个方法是正常结東还是异常结束。

    从上面的指令代码中可以看到,为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行momtorexit指令。

    JVM指令集大全

    下面三个为保留操作码,是留给虚拟机内部使用的。

    0xca breakpoint  调试时的断点标记
    0xfe impdep1    为特定软件而预留的语言后门
    0xff impdep2    为特定硬件而预留的语言后门

    下面是所有的JVM指令集,可能个别会有出入,可以参看下《Java虚拟机规范 (JavaSE8版)》的第七章操作码助记符。

    指令码 助记符    说明
    0x00 nop        无操作
    0x01 aconst_null 将null推送至栈顶
    0x02 iconst_m1    将int型-1推送至栈顶
    0x03 iconst_0    将int型0推送至栈顶
    0x04 iconst_1    将int型1推送至栈顶
    0x05 iconst_2    将int型2推送至栈顶
    0x06 iconst_3    将int型3推送至栈顶
    0x07 iconst_4    将int型4推送至栈顶
    0x08 iconst_5    将int型5推送至栈顶
    0x09 lconst_0    将long型0推送至栈顶
    0x0a lconst_1    将long型1推送至栈顶
    0x0b fconst_0    将float型0推送至栈顶
    0x0c fconst_1    将float型1推送至栈顶
    0x0d fconst_2    将float型2推送至栈顶
    0x0e dconst_0    将double型0推送至栈顶
    0x0f dconst_1    将double型1推送至栈顶
    0x10 bipush    将单字节的常量值(-128~127)推送至栈顶
    0x11 sipush    将一个短整型常量值(-32768~32767)推送至栈顶
    0x12 ldc    将int, float或String型常量值从常量池中推送至栈顶
    0x13 ldc_w    将int, float或String型常量值从常量池中推送至栈顶(宽索引)
    0x14 ldc2_w    将long或double型常量值从常量池中推送至栈顶(宽索引)
    0x15 iload    将指定的int型本地变量推送至栈顶
    0x16 lload    将指定的long型本地变量推送至栈顶
    0x17 fload    将指定的float型本地变量推送至栈顶
    0x18 dload    将指定的double型本地变量推送至栈顶
    0x19 aload    将指定的引用类型本地变量推送至栈顶
    0x1a iload_0    将第一个int型本地变量推送至栈顶
    0x1b iload_1    将第二个int型本地变量推送至栈顶
    0x1c iload_2    将第三个int型本地变量推送至栈顶
    0x1d iload_3    将第四个int型本地变量推送至栈顶
    0x1e lload_0    将第一个long型本地变量推送至栈顶
    0x1f lload_1    将第二个long型本地变量推送至栈顶
    0x20 lload_2    将第三个long型本地变量推送至栈顶
    0x21 lload_3    将第四个long型本地变量推送至栈顶
    0x22 fload_0    将第一个float型本地变量推送至栈顶
    0x23 fload_1    将第二个float型本地变量推送至栈顶
    0x24 fload_2    将第三个float型本地变量推送至栈顶
    0x25 fload_3    将第四个float型本地变量推送至栈顶
    0x26 dload_0    将第一个double型本地变量推送至栈顶
    0x27 dload_1    将第二个double型本地变量推送至栈顶
    0x28 dload_2    将第三个double型本地变量推送至栈顶
    0x29 dload_3    将第四个double型本地变量推送至栈顶
    0x2a aload_0    将第一个引用类型本地变量推送至栈顶
    0x2b aload_1    将第二个引用类型本地变量推送至栈顶
    0x2c aload_2    将第三个引用类型本地变量推送至栈顶
    0x2d aload_3    将第四个引用类型本地变量推送至栈顶
    0x2e iaload    将int型数组指定索引的值推送至栈顶
    0x2f laload    将long型数组指定索引的值推送至栈顶
    0x30 faload    将float型数组指定索引的值推送至栈顶
    0x31 daload    将double型数组指定索引的值推送至栈顶
    0x32 aaload    将引用型数组指定索引的值推送至栈顶
    0x33 baload    将boolean或byte型数组指定索引的值推送至栈顶
    0x34 caload    将char型数组指定索引的值推送至栈顶
    0x35 saload    将short型数组指定索引的值推送至栈顶
    0x36 istore    将栈顶int型数值存入指定本地变量
    0x37 lstore    将栈顶long型数值存入指定本地变量
    0x38 fstore    将栈顶float型数值存入指定本地变量
    0x39 dstore    将栈顶double型数值存入指定本地变量
    0x3a astore    将栈顶引用型数值存入指定本地变量
    0x3b istore_0    将栈顶int型数值存入第一个本地变量
    0x3c istore_1    将栈顶int型数值存入第二个本地变量
    0x3d istore_2    将栈顶int型数值存入第三个本地变量
    0x3e istore_3    将栈顶int型数值存入第四个本地变量
    0x3f lstore_0    将栈顶long型数值存入第一个本地变量
    0x40 lstore_1    将栈顶long型数值存入第二个本地变量
    0x41 lstore_2    将栈顶long型数值存入第三个本地变量
    0x42 lstore_3    将栈顶long型数值存入第四个本地变量
    0x43 fstore_0    将栈顶float型数值存入第一个本地变量
    0x44 fstore_1    将栈顶float型数值存入第二个本地变量
    0x45 fstore_2    将栈顶float型数值存入第三个本地变量
    0x46 fstore_3    将栈顶float型数值存入第四个本地变量
    0x47 dstore_0    将栈顶double型数值存入第一个本地变量
    0x48 dstore_1    将栈顶double型数值存入第二个本地变量
    0x49 dstore_2    将栈顶double型数值存入第三个本地变量
    0x4a dstore_3    将栈顶double型数值存入第四个本地变量
    0x4b astore_0    将栈顶引用型数值存入第一个本地变量
    0x4c astore_1    将栈顶引用型数值存入第二个本地变量
    0x4d astore_2    将栈顶引用型数值存入第三个本地变量
    0x4e astore_3    将栈顶引用型数值存入第四个本地变量
    0x4f iastore    将栈顶int型数值存入指定数组的指定索引位置
    0x50 lastore    将栈顶long型数值存入指定数组的指定索引位置
    0x51 fastore    将栈顶float型数值存入指定数组的指定索引位置
    0x52 dastore    将栈顶double型数值存入指定数组的指定索引位置
    0x53 aastore    将栈顶引用型数值存入指定数组的指定索引位置
    0x54 bastore    将栈顶boolean或byte型数值存入指定数组的指定索引位置
    0x55 castore    将栈顶char型数值存入指定数组的指定索引位置
    0x56 sastore    将栈顶short型数值存入指定数组的指定索引位置
    0x57 pop     将栈顶数值弹出 (数值不能是long或double类型的)
    0x58 pop2    将栈顶的一个(long或double类型的)或两个数值弹出(其它)
    0x59 dup     复制栈顶数值并将复制值压入栈顶
    0x5a dup_x1    复制栈顶数值并将两个复制值压入栈顶
    0x5b dup_x2    复制栈顶数值并将三个(或两个)复制值压入栈顶
    0x5c dup2    复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
    0x5d dup2_x1    复制栈顶的一个或两个值,将其插入栈顶那两个或三个值的下面
    0x5e dup2_x2    复制栈顶的一个或两个值,将其插入栈顶那两个、三个或四个值的下面
    0x5f swap    将栈最顶端的两个数值互换(数值不能是long或double类型的)
    0x60 iadd    将栈顶两int型数值相加并将结果压入栈顶
    0x61 ladd    将栈顶两long型数值相加并将结果压入栈顶
    0x62 fadd    将栈顶两float型数值相加并将结果压入栈顶
    0x63 dadd    将栈顶两double型数值相加并将结果压入栈顶
    0x64 isub    将栈顶两int型数值相减并将结果压入栈顶
    0x65 lsub    将栈顶两long型数值相减并将结果压入栈顶
    0x66 fsub    将栈顶两float型数值相减并将结果压入栈顶
    0x67 dsub    将栈顶两double型数值相减并将结果压入栈顶
    0x68 imul    将栈顶两int型数值相乘并将结果压入栈顶
    0x69 lmul    将栈顶两long型数值相乘并将结果压入栈顶
    0x6a fmul    将栈顶两float型数值相乘并将结果压入栈顶
    0x6b dmul    将栈顶两double型数值相乘并将结果压入栈顶
    0x6c idiv    将栈顶两int型数值相除并将结果压入栈顶
    0x6d ldiv    将栈顶两long型数值相除并将结果压入栈顶
    0x6e fdiv    将栈顶两float型数值相除并将结果压入栈顶
    0x6f ddiv    将栈顶两double型数值相除并将结果压入栈顶
    0x70 irem    将栈顶两int型数值作取模运算并将结果压入栈顶
    0x71 lrem    将栈顶两long型数值作取模运算并将结果压入栈顶
    0x72 frem    将栈顶两float型数值作取模运算并将结果压入栈顶
    0x73 drem    将栈顶两double型数值作取模运算并将结果压入栈顶
    0x74 ineg    将栈顶int型数值取负并将结果压入栈顶
    0x75 lneg    将栈顶long型数值取负并将结果压入栈顶
    0x76 fneg    将栈顶float型数值取负并将结果压入栈顶
    0x77 dneg    将栈顶double型数值取负并将结果压入栈顶
    0x78 ishl    将int型数值左移位指定位数并将结果压入栈顶
    0x79 lshl    将long型数值左移位指定位数并将结果压入栈顶
    0x7a ishr    将int型数值右(符号)移位指定位数并将结果压入栈顶
    0x7b lshr    将long型数值右(符号)移位指定位数并将结果压入栈顶
    0x7c iushr    将int型数值右(无符号)移位指定位数并将结果压入栈顶
    0x7d lushr    将long型数值右(无符号)移位指定位数并将结果压入栈顶
    0x7e iand    将栈顶两int型数值作“按位与”并将结果压入栈顶
    0x7f land    将栈顶两long型数值作“按位与”并将结果压入栈顶
    0x80 ior     将栈顶两int型数值作“按位或”并将结果压入栈顶
    0x81 lor     将栈顶两long型数值作“按位或”并将结果压入栈顶
    0x82 ixor    将栈顶两int型数值作“按位异或”并将结果压入栈顶
    0x83 lxor    将栈顶两long型数值作“按位异或”并将结果压入栈顶
    0x84 iinc    将指定int型变量增加指定值(i++, i--, i+=2)
    0x85 i2l     将栈顶int型数值强制转换成long型数值并将结果压入栈顶
    0x86 i2f     将栈顶int型数值强制转换成float型数值并将结果压入栈顶
    0x87 i2d     将栈顶int型数值强制转换成double型数值并将结果压入栈顶
    0x88 l2i     将栈顶long型数值强制转换成int型数值并将结果压入栈顶
    0x89 l2f     将栈顶long型数值强制转换成float型数值并将结果压入栈顶
    0x8a l2d     将栈顶long型数值强制转换成double型数值并将结果压入栈顶
    0x8b f2i     将栈顶float型数值强制转换成int型数值并将结果压入栈顶
    0x8c f2l     将栈顶float型数值强制转换成long型数值并将结果压入栈顶
    0x8d f2d     将栈顶float型数值强制转换成double型数值并将结果压入栈顶
    0x8e d2i     将栈顶double型数值强制转换成int型数值并将结果压入栈顶
    0x8f d2l     将栈顶double型数值强制转换成long型数值并将结果压入栈顶
    0x90 d2f     将栈顶double型数值强制转换成float型数值并将结果压入栈顶
    0x91 i2b     将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
    0x92 i2c     将栈顶int型数值强制转换成char型数值并将结果压入栈顶
    0x93 i2s     将栈顶int型数值强制转换成short型数值并将结果压入栈顶
    0x94 lcmp    比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶
    0x95 fcmpl    比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
    0x96 fcmpg    比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
    0x97 dcmpl    比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
    0x98 dcmpg    比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
    0x99 ifeq    当栈顶int型数值等于0时跳转
    0x9a ifne    当栈顶int型数值不等于0时跳转
    0x9b iflt    当栈顶int型数值小于0时跳转
    0x9c ifge    当栈顶int型数值大于等于0时跳转
    0x9d ifgt    当栈顶int型数值大于0时跳转
    0x9e ifle    当栈顶int型数值小于等于0时跳转
    0x9f if_icmpeq    比较栈顶两int型数值大小,当结果等于0时跳转
    0xa0 if_icmpne    比较栈顶两int型数值大小,当结果不等于0时跳转
    0xa1 if_icmplt    比较栈顶两int型数值大小,当结果小于0时跳转
    0xa2 if_icmpge    比较栈顶两int型数值大小,当结果大于等于0时跳转
    0xa3 if_icmpgt    比较栈顶两int型数值大小,当结果大于0时跳转
    0xa4 if_icmple    比较栈顶两int型数值大小,当结果小于等于0时跳转
    0xa5 if_acmpeq    比较栈顶两引用型数值,当结果相等时跳转
    0xa6 if_acmpne    比较栈顶两引用型数值,当结果不相等时跳转
    0xa7 goto    无条件跳转
    0xa8 jsr     跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
    0xa9 ret     返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用)
    0xaa tableswitch    用于switch条件跳转,case值连续(可变长度指令)
    0xab lookupswitch    用于switch条件跳转,case值不连续(可变长度指令)
    0xac ireturn    从当前方法返回int
    0xad lreturn    从当前方法返回long
    0xae freturn    从当前方法返回float
    0xaf dreturn    从当前方法返回double
    0xb0 areturn    从当前方法返回对象引用
    0xb1 return    从当前方法返回void
    0xb2 getstatic    获取指定类的静态域,并将其值压入栈顶
    0xb3 putstatic    为指定的类的静态域赋值
    0xb4 getfield    获取指定类的实例域,并将其值压入栈顶
    0xb5 putfield    为指定的类的实例域赋值
    0xb6 invokevirtual    调用实例方法
    0xb7 invokespecial    调用超类构造方法,实例初始化方法,私有方法
    0xb8 invokestatic    调用静态方法
    0xb9 invokeinterface 调用接口方法
    0xba invokedynamic  调用动态链接方法
    0xbb new     创建一个对象,并将其引用值压入栈顶
    0xbc newarray    创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
    0xbd anewarray    创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
    0xbe arraylength 获得数组的长度值并压入栈顶
    0xbf athrow    将栈顶的异常抛出
    0xc0 checkcast    检验类型转换,检验未通过将抛出ClassCastException
    0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
    0xc2 monitorenter    获得对象的锁,用于同步方法或同步块
    0xc3 monitorexit    释放对象的锁,用于同步方法或同步块
    0xc4 wide    扩大本地变量索引的宽度
    0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶
    0xc6 ifnull    为null时跳转
    0xc7 ifnonnull    不为null时跳转
    0xc8 goto_w    无条件跳转
    0xc9 jsr_w    跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶
    ============================================
    0xca breakpoint  调试时的断点标记
    0xfe impdep1    为特定软件而预留的语言后门
    0xff impdep2    为特定硬件而预留的语言后门
    最后三个为保留指令

    参考:《深入理解Java虚拟机第二版》、《Java虚拟机规范 JavaSE8版》

    展开全文
  • 字节码指令简介

    2019-01-13 22:59:31
    字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构: 由于限定了Java虚拟机操作码的长度为1个字节,指令集的操作码不能超过256条。 Class文件格式放弃了编译后代码中操作数长度对齐,这就意味者虚拟机...

    Java虚拟机的指令由一个字节长度的、 代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

    字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构:

    1. 由于限定了Java虚拟机操作码的长度为1个字节,指令集的操作码不能超过256条。
    2. Class文件格式放弃了编译后代码中操作数长度对齐,这就意味者虚拟机处理那些超过一个字节数据的时候,不得不在运行的时候从字节码中重建出具体数据的结构。

    这种操作在一定程度上会降低一些性能,但这样做的优势也非常的明显:

    1. 放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号
    2. 用一个字节来表示操作码,也是为了获取短小精悍的代码

    Java虚拟机解释器执行简单模型如下:

    do{  
            自动计算PC寄存器的值+1;  
            根据PC寄存器指示位置,从字节码流中取出操作码;  
            if(存在操作数) 从字节码流中取出操作数;  
            执行操作码定义的操作;  
        }while(字节码流长度>0);  

    字节码与数据类型

    在Java虚拟机指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload 指令用于从局部变量表中加载int 型的数据到操作数栈中。

    但是由于虚拟机操作码长度只有一个字节,所以包含了数据类型的操作码就为指令集的设计带来了很大的压力:如果每一种数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令集的数据就会超过256个了。因此虚拟机只提供了有限的指令集来支持所有的数据类型。

    如load 操作, 只有iload、lload、fload、dload、aload用来支持int、long、float、double、reference 类型的入栈,而对于boolean 、byte、short 和char 则没有专门的指令来进行运算。编译器会在编译期或运行期将byte 和 short 类型的数据带符号扩展为int类型的数据,将boolean 和 char 类型的数据零位扩展为相应的int 类型数据。与之类似,在处理boolean、byte、short 和 char 类型的数组时,也会发生转换。因此,大多数对于boolean、byte、short 和char 类型数据的擦操作,实际上都是使用相应的int 类型作为运算类型。

     

    加载和存储指令

    加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。
            1)将一个局部变量加载到操作数栈的指令包括:iload,iload_<n>,lload、lload_<n>、float、 fload_<n>、dload、dload_<n>,aload、aload_<n>。
            2)将一个数值从操作数栈存储到局部变量表的指令:istore,istore_<n>,lstore,lstore_<n>,fstore,fstore_<n>,dstore,dstore_<n>,astore,astore_<n>
            3)将常量加载到操作数栈的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
            4)局部变量表的访问索引指令:wide
    一部分以尖括号结尾的指令代表了一组指令,如iload_<i>,代表了iload_0,iload_1等,这几组指令都是带有一个操作数的通用指令。

    运算指令

    算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
            1)加法指令:iadd,ladd,fadd,dadd
            2)减法指令:isub,lsub,fsub,dsub
            3)乘法指令:imul,lmul,fmul,dmul
            4)除法指令:idiv,ldiv,fdiv,ddiv
            5)求余指令:irem,lrem,frem,drem
            6)取反指令:ineg,leng,fneg,dneg
            7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr
            8)按位或指令:ior,lor
            9)按位与指令:iand,land
            10)按位异或指令:ixor,lxor
            11)局部变量自增指令:iinc
            12)比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
    Java虚拟机没有明确规定整型数据溢出的情况,但规定了处理整型数据时,只有除法和求余指令出现除数为0时会导致虚拟机抛出异常。

    Java虚拟机要求在浮点数运算的时候,所有结果否必须舍入到适当的精度,如果有两种可表示的形式与该值一样,会优先选择最低有效位为零的。称之为最接近数舍入模式。

    浮点数向整数转换的时候,Java虚拟机使用IEEE 754标准中的向零舍入模式,这种模式舍入的结果会导致数字被截断,所有小数部分的有效字节会被丢掉。

     

    类型转换指令

    类型转换指令将两种Java虚拟机数值类型相互转换,这些操作一般用于实现用户代码的显式类型转换操作。
    JVM直接就支持宽化类型转换(小范围类型向大范围类型转换):
            1)int类型到long,float,double类型
            2)long类型到float,double类型
            3)float到double类型

    但在处理窄化类型转换时,必须显式使用转换指令来完成,这些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。

    将int 或 long 窄化为整型T的时候,仅仅简单的把除了低位的N个字节以外的内容丢弃,N是T的长度。这有可能导致转换结果与输入值有不同的正负号。

    在将一个浮点值窄化为整数类型T(仅限于 int 和 long 类型),将遵循以下转换规则:

            1)如果浮点值是NaN , 呐转换结果就是int 或 long 类型的0

            2)如果浮点值不是无穷大,浮点值使用IEEE 754 的向零舍入模式取整,获得整数v, 如果v在T表示范围之内,那就过就是v

            3)否则,根据v的符号, 转换为T 所能表示的最大或者最小正数

     

    对象创建与访问指令

    虽然类实例和数组都是对象,Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。
            1)创建实例的指令:new
            2)创建数组的指令:newarray,anewarray,multianewarray
            3)访问字段指令:getfield,putfield,getstatic,putstatic
            4)把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload
            5)将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
            6)取数组长度指令:arraylength JVM支持方法级同步和方法内部一段指令序列同步,这两种都是通过moniter实现的。
            7)检查实例类型指令:instanceof,checkcast

     

    操作数栈管理指令

    如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:

            1)将操作数栈的栈顶一个或两个元素出栈:pop、pop2

            2)复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。

            3)将栈最顶端的两个数值互换:swap

    控制转移指令

    让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:
            1)条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等
            2)复合条件分支:tableswitch,lookupswitch
            3)无条件分支:goto,goto_w,jsr,jsr_w,ret

    JVM中有专门的指令集处理int和reference类型的条件分支比较操作,为了可以无明显标示一个实体值是否是null,有专门的指令检测null 值。boolean类型和byte类型,char类型和short类型的条件分支比较操作,都使用int类型的比较指令完成,而 long,float,double条件分支比较操作,由相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件比较操作完成整个分支跳转。各种类型的比较都最终会转化为int类型的比较操作。
     

    方法调用和返回指令

    invokevirtual指令:调用对象的实例方法,根据对象的实际类型进行分派(虚拟机分派)。
    invokeinterface指令:调用接口方法,在运行时搜索一个实现这个接口方法的对象,找出合适的方法进行调用。
    invokespecial:调用需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
    invokestatic:调用类方法(static)
    方法返回指令是根据返回值的类型区分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn,freturn,drturn和areturn,另外一个return供void方法,实例初始化方法,类和接口的类初始化i方法使用。

     

    异常处理指令

    在Java程序中显式抛出异常的操作(throw语句)都有athrow 指令来实现,除了用throw 语句显示抛出异常情况外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

    在Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。

     

    同步指令


    方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,然后执行方法,最后完成方法时释放moniter。
    同步一段指令集序列,通常由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。
    结构化锁定是指方法调用期间每一个monitor退出都与前面monitor进入相匹配的情形。JVM通过以下两条规则来保证结结构化锁成立(T代表一线程,M代表一个monitor):
            1)T在方法执行时持有M的次数必须与T在方法完成时释放的M次数相等
            2)任何时刻都不会出现T释放M的次数比T持有M的次数多的情况

     

    资料:深入理解Java虚拟机

    展开全文
  • 本篇文章对Java字节码指令集的使用进行了详细的介绍。需要的朋友参考下
  • JVM 字节码指令手册 - 查看 Java 字节码

    千次阅读 多人点赞 2019-08-15 21:26:27
    JVM 字节码指令手册 - 查看 Java 字节码 jdk 进行的编译生成的 .class 是 16 进制数据文件,不利于学习分析。通过下命令 javap -c Demo.class > Demo.txt 或者其他方式可反汇编,得到字节码文件 一、JVM 指令...
  • 2、字节码指令 可参考: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5 接着上一节,研究一下两组字节码指令,一个是 public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令: ...
  • class 类文件结构与字节码指令

    千次阅读 2020-02-22 19:11:03
     可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。 Java虚拟机直接支持以下数值类型的...
  • Java常用的字节码指令

    千次阅读 2018-07-19 13:38:16
    Java虚拟机的指令是由一个字节长度的,代表着某种特定操作含义的数字,称之为操作码,以及 跟随其后的0至多个代表...字节码和数据类型 在虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,如( ...
  • Class字节码指令解释执行

    千次阅读 2017-09-17 16:00:19
    JVM指令主要包含了一下几种类型:加载和存储指令、运算指令、类型转换指令、对象创建与访问指令、操作数栈管理指令、控制转移指令、方法调用和返回指令、异常处理指令、同步指令等。 基于栈的解释器执行过程  下面...
  • 操作:一个字节长度(0~255),意味着指令集的操作个数不能操作256条。 操作数:一条指令可以有零或者多个操作数,且操作数可以是1个或者多个字节。编译后的代码没有采用操作数长度对齐方式,比如16位无符号整数需...
  • 什么是字节码指令

    千次阅读 2018-11-05 23:23:52
    字节码指令简介:  Java虚拟机的指令由一个字节长度的、代表着某种特定含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。 由于Java虚拟机采用面向操作数...
  • 字节码指令简介(笔记)

    千次阅读 2018-08-28 20:49:06
    Java虚拟机指令的组成:操作码(Opcode,...字节码指令集的特点:A、指令集的操作码总数不可能超过256条;B、当数据大小超过一个字节时,Java虚拟机需要重构出具体数据的结构。(比如:将一个16位长度的无符号整数使...
  • Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,“Java 字节码难学吗?我能不能学会啊?” 讲良心话,不是我谦虚,一开始学 Java 字节码和 Java 虚拟机方面的知识我也感觉头大!...
  • JVM字节码执行模型及字节码指令

    万次阅读 2015-06-19 16:25:05
    JVM执行模型,是如何把Class文件里的字节码转换成我们的虚拟机栈的操作指令,以及整个虚拟机栈的内部数据结构是怎样的,这篇文章后续会详细介绍,并且稍微扩展下JVM规范中的一些字节码指令集。
  • 主要介绍了Java中invokedynamic字节码指令问题,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
  • 有哪些常见的字节码指令

    千次阅读 热门讨论 2021-05-25 22:59:40
    写在前面 本文隶属于专栏《100个问题搞定Java...Java 字节码可以划分为很多种类型,如加载常量指令,操作数栈专用指令,局部变量区访问指令,Java 相关指令,方法调用指令,数组相关指令,控制流指令,以及计算相关
  • 查看jvm运行时字节码指令

    千次阅读 2018-11-18 00:21:00
    java方法编译后会生成字节码指令,在运行期字节码指令会被加载到JVM内存中,使用HSDB可以查看运行期的字节码指令 贴代码: public class Test extends BaseClass { private Integer i=3; private static int a...
  • Java字节码指令简介

    千次阅读 2016-06-04 12:02:30
    存储在Code属性中的是字节码,也就是编译后的程序。Java虚拟机的指令由两部分组成,首先是一个字节长度、代表某种含义的数字(即操作码),在操作码后面跟着零个或多个代表这个操作所需的参数(即操作数)。由于Java...
  • 方法调用指令 invokevirtual 该指令用于调用对象的实例方法,包括public方法和protected方法 invokeinterface 该指令用于调用接口方法 invokespecial 该指令用于调用一些需要特殊处理的实例方法,包括...
  • 第2章:字节码指令集与解析举例.mmap
  • Java字节码指令列表

    热门讨论 2011-05-29 00:05:10
    Java字节码指令列表,列出了每条指令的操作码和操作数,和对栈的操作情况
  • JVM字节码指令简介

    2018-09-28 23:03:33
    所以Java字节码指令对应汇编语言,Java字节码指令集对应汇编指令集。 字节码简介 Java字节码指令由 一个 字节长度的,代表某种特定操作含义的数字(操作码)以及其后的零至多个代表此操作所需参数(操作数)。...
  • JVM字节码指令之对象创建和操作

    千次阅读 2019-07-05 00:33:09
    在 Java 中 new 是一个关键字,在字节码中也有一个指令 new。当我们创建一个对象时,背后发生了哪些事情呢? ScoreCalculator calculator = new ScoreCalculator(); 对应的字节码如下: 0: new #2 // class ...
  • 从java反编译学习字节码指令(一)

    千次阅读 2018-07-31 21:51:35
    最近沉迷于java反编译,经常看到iconst_1指令,见得多了,也让我...做个小测试,从11到0,看看它们分别对应字节码什么? public class Bytecode { public void ByteCode() { int eleven = 11; int ten = 10; ...
  • Python 2.6.2的字节码指令集一览

    千次阅读 2017-12-02 12:28:41
    对Python的字节码指令集感兴趣但不知道从何下手么?执行这段代码就能看到字节码的列表:  Python代码  import opcode  for op in range(len(opcode.opname)):   print('0x%.2X(%.3d): %s'...
  • Bytecode 字节码指令 jvm 内部细节 本文分为三部分,每一部分都分成几个小节。 每个小节都可以单独阅读,不过由于一些概念是逐步建立起来的,如果你依次阅读完所有章节会更简单一些。 每一节都会覆盖到Java代码中的...
  • JVM字节码指令

    千次阅读 2020-05-12 14:43:23
    字节码指令 JVM字节码指令由一个字节长度,包含着特定含义的数值(操作码)以及跟随其后的零至多个操作所需参数(操作数)所构成;大多数字节码指令只有一个操作码,没有操作数,一般都是将操作所需参数存入操作数...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 218,162
精华内容 87,264
关键字:

字节码指令