线程和栈帧

要了解 ASM 字节码操作,先要熟悉 jvm 线程与栈帧结构,jvm 开辟一个线程,便会开辟属于这个线程虚拟机栈,本地方法栈,程序计数器,其主要作用如下:

  • 虚拟机栈: 以栈帧为基本单位,一个栈帧的开始地址代表一个方法的入口,栈帧里面有操作数栈,局部变量表,动态链接,方法出口,其他信息;其说明如下:
    • 操作数栈:: 用来存放需要 cpu 进行计算的基本类型和对象引用,以 4 个字节的 slot 为基本单位;
    • 局部变量表:存放当前栈帧涉及到的变量表,以 4 个字节的 slot 为基本单位;
    • 动态链接: 存放执行常量池的引用,可以实现当前方法的动态链接;
    • 方法出口:用于方法的返回,可以是异常的发生,return 操作指令等等;
  • 本地方法栈:类似于虚拟机栈,但是保存的是 java 本地代码库的一些信息;
  • 程序计数器:保存该线程指令执行位置,待下次调度时可以接着往下执行;
    具体如下图所示:
    ASM基本概念 - 图1
    线程虚拟机栈结构

基本类型

java 字节码中的类型表达和 java 代码是不一样的,在 java 字节码中利用如下符号来表达基本类型:

java 类型 type 含义
boolean Z 布尔
char C 字符
byte B 字节
short S 短整型
int I 整型
long J 长整型
float F 浮点数
reference L 类的引用
void V
double D 双精度浮点型
Object Ljava/lang/Object; 对象
int[] [I 整型数组
Object[][] [[Ljava/lang/Object; 对象数组

注: L+className; 代表某类的引用 (“;” 不能省略)

字节码实例:

Java 代码 字节码表示 注释
double[][] [[D
Object run(int i,double d,Thread t) (IDLjava/lang/Thread)Ljava/lang/Object; (方法参数字节码类型) 方法返回参数类型

字节码指令操作

字节码指令操作其实主要操作局部变量表和操作数栈,具体流程是:load 局部变量到操作数栈,然后给 cpu 下达执行指令,然后将操作数栈栈顶元素弹出,从而实现一个操作;
字节码指令都有一定的格式:[type+]op[“_”+value];

其中 type 根据基本类型可以为:i(int 整数),s(short 短整数),b(byte 字节),c(char 字符),l(long 长整数),d(double 双精度浮点数),f(float 浮点数),a(reference 引用); value 指的是操作数,如果操作数为负数时需要添加’m” 前缀, 例如 iconstant_m1 表示将 - 1 压栈;如果操作数值超过一定大小,则会将该操作数存放在常量池,用 #indexbyte 表示其位置; op 指的是操作码,通常用一个字节表示;

字节码指令操作主要分为九大指令:

  • 加载和存储指令:用于将数据在操作数栈和局部变量表来回传输;
  • 运算指令:用于将操作数栈栈顶的两个数值进行运算,然后重新放入操作数栈顶;
  • 类型转换指令:用于将两个不同数值类型进行相互转换;
  • 对象创建和访问指令: 用于创建对象和访问对象
  • 操作数栈管理指令:用于管理操作数栈,类似普通栈管理
  • 控制转移指令:用于让 jvm 从指定位置的指令开始执行而不是控制下一条指令位置开始执行;
  • 方法调用和返回指令:用于方法调用和方法返回;
  • 异常处理指令:对 jvm 抛出的异常进行处理指令;
  • 方法同步指令:用来控制不同线程对方法的同步控制;

加载和存储指令

加载指令主要是将局部变量和常量压入到操作数栈,具体指令有:

  • 常量压栈指令,常量压栈指令时根据常量所占字节大小划分,指令如下:
    • constant: 该常量字节大小为 - 1 到 5 的数值,例如 iconstant_0 将整数 0 压栈,lconstant_5 将长整数 5 压栈;
    • bipush: 将字节值为 byte 类型的数值转换为整型,然后压栈 (byte 值大小为 - 128-127);
    • sipush: 将字节值为 short 类型的数值转换为整型,然后压栈 (short 值为 - 32768-32767);
    • ldc: 根据指定索引值 (需要一个字节存储的 indexbyte) 从常量池取出大小在 - 2147483648~2147483647 的常量值,如 int,float,Reference 型常量值;
    • ldc_w: 根据指定宽索引值 (需要两个字节存储的索引值) 从常量池取出如 int,float,Reference 等常量值进行压栈;
    • ldc2_w: 根据指定宽索引值从常量池中取出如 long,double 等常量值进行压栈;
      如下图:
      ASM基本概念 - 图2
      常量压栈. png
      如上图所示,其中 32767 表示操作数值,在[-32768,32767]之间是不会保存到常量池的,而超过这个值则需要利用 indexbyte(#30,#35) 代表的索引,去常量池中查找
  • load: 将局部变量指定位置 (具体值或者索引) 处的对象压栈;aload_0 将局部变量表 0 处的引用类型入栈,
    iload indexbyte 将局部变量表中 indexbyte 表示的 int 类型入栈;caload 从 char 类型数组中装载指定项的值(先转换为 int 类型值,后压栈)
  • store : 将操作数栈栈顶值弹出并保存到局部变量表中; 例如:istore_3 将 short,byte,char,int 类型保存到局部变量表 3 处根据类型转换,lstore [opNum] (opNum 需大于 3) 则将 long 类型保存到局部变量 opNum 处;dstore 用来保存栈顶的 double 类型,fstore 用来保存栈顶的 float 类型;

如下图所示:

ASM基本概念 - 图3

加载存储指令. png

运算指令

运算指令有以下几种:

  • (T)add: 将栈顶 T 类型的两个数值相加后入栈,T:float,int,short,long,double
  • (T)sub: 将栈顶 T 类型的两个数值相减后入栈,T:float,int,short,long,double
  • (T)mul: 将栈顶 T 类型的两个数值相乘后入栈,T:float,int,short,long,double
  • (T)div: 将栈顶 T 类型的两个数值相除后入栈,T:float,int,short,long,double
  • (T)rem: 将栈顶 T 类型的两个数值取模后入栈,T:float,int,short,long,double
  • (T)neg: 将栈顶 T 类型的取负后入栈,T:float,int,short,long,double
  • (T)iinc [indexbyte,constantbyte]: 将整数值 constbyte 加到 indexbyte 指定的 int 类型的局部变量中;
    ASM基本概念 - 图4
    运算指令. png
  • (T)shl: 算数左移后入栈,T 为非浮点类型的基本类型;
  • (T)shr: 算数左移后入栈,T 为非浮点类型的基本类型;
  • (T)ushl: 逻辑左移后入栈,T 为非浮点类型的基本类型;
  • (T)ushr: 逻辑右移后入栈, T 为非浮点类型的基本类型;
  • (T)and: 与操作,T 为非浮点类型的基本类型;
  • (T)or: 或操作,T 为非浮点类型的基本类型;
  • (T)xor: 异或操作, T 为非浮点类型的基本类型;

类型转换指令

类型转换指令有以下几种:

  • (T)2(V): 将 T 基本类型转换成 V 基本类型,如果是长字节类型转换短字节类型,则需要把高位字节截断;如 l2i: 将 long 转换成 int 则会把高 4 个字节截断后剩下的四个字节转换成 int;

对象创建和访问指令

对象创建和访问指令通常需要两个操作数 indexbyte1 和 indexbyte2

  • new : 创建新的对象实例;
  • checkcast: 强制类型转换;
  • instanceof: 判断是否类实例;
  • getField: 获取类实例字段值;
  • putField: 给类实例字段赋值;
  • getStatic: 获取类静态变量值;
  • putStatic: 给类静态变量赋值;
  • newarray: 创建基本类型数组;
  • anewarray: 创建引用类型数组;
  • arraylength: 获取一维数组长度;

操作数栈管理指令

字等于两个字节,半个 slot,16 位

  • nop: 空操作;
  • pop : 弹出栈顶一个字长数据;
  • pop2: 弹出栈顶两个字长的数据;
  • dup: 复制栈顶一个字长的数据,同时将该数据入栈;
  • dup_x1:复制栈顶一个字长的数据,同时弹出栈顶两个字长的数据,然后再将复制的数据入栈,再将弹出的两个字入栈;
  • dup_x2: 复制栈顶一个字长的数据,同时弹出栈顶三个字长的数据,然后再将复制的数据入栈,再将弹出的三个字入栈;
  • dup2: 复制栈顶两个字长的数据,同时将该数据入栈;
  • dup2_x1: 复制栈顶两个字长的数据,同时弹出栈顶三个字长的数据,然后再将复制的数据入栈,再将弹出的三个字入栈;
  • dup2_x2: 复制栈顶两个字长的数据,同时弹出栈顶四个字长的数据,然后再将复制的数据入栈,再将弹出的四个字入栈;
  • swap: 交换栈顶两个字长的数据,Java 指令中没有提供交换两个字长为单位的交换指令;

控制转移指令:

控制转移指令分为跳转指令和比较指令,无条件跳转指令, 表跳转指令,异常跳转指令;
跳转指令:

  • ifeq: 若栈顶 int 类型为 0 则跳转;
  • ifne: 若栈顶 int 类型不为 0 则跳转;
  • iflt: 若栈顶 int 类型小于 0 则跳转;
  • ifle: 若栈顶 int 类型小于等于 0 则跳转;
  • ifgt: 若栈顶 int 类型大于 0 则跳转;
  • ifge: 若栈顶 int 类型大于等于 0 则跳转;
  • if_icmpeq: 若栈顶两 int 类型相等则跳转;
  • if_icmpne: 若栈顶两 int 类型相等则跳转;
  • if_icmplt: 若栈顶 int 前小于后则跳转;
  • if_icpmle: 若栈顶 int 前小于等于后则跳转;
  • if_icpmgt: 若栈顶 int 前大于后则跳转;
  • if_icpmge: 若栈顶 int 前大于等于后则跳转;
  • ifnull: 如栈顶引用为空则跳转;
  • ifnonnull: 若栈顶引用不为空则跳转;
  • if_acmpeq: 若栈顶两引用相等则跳转;
  • if_acmpne: 若栈顶两引用不相等则跳转;

比较指令:

  • (T)cmp: 比较栈顶两个 T 类型大小,前者大,则 1 入栈;相等则 0 入栈;后者大则 - 1 入栈;
  • (T)cmpl: 比较栈顶两个 T 类型大小,前者大,则 1 入栈;相等则 0 入栈;后者大则 - 1 入栈;有 NAN 存在,则 - 1 入栈;
  • (T)cmpg: 比较栈顶两个 T 类型大小,前者大,则 1 入栈;相等则 0 入栈;后者大则 - 1 入栈;有 NAN 存在,则 - 1 入栈;

无条件转移指令:

  • goto : 无条件转移到指定位置;
  • goto_w: 无条件转移到指定位置 (宽索引);

表跳转指令:

  • tableswitch: 通过索引访问跳转表,并跳转;
  • lookupswitch: 通过健值访问跳转表,并跳转;

异常跳转指令:

  • athrow: 抛出异常;
  • jsr: 跳转到指定程序;
  • jsr_w: 跳转到指定程序 (宽索引);
  • ret: 返回到指定程序;

方法调用和返回指令:

  • invokerspecial: 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法,编译时绑定;
  • invokevirtual: 指令用于调用对象的实例方法,根据对象的实际类型进行分派,运行时绑定;
  • invokestatic: 调用静态方法;
  • invokeinterface: 用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用;
  • invokedynamic: 用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断, 从而达到动态语言的支持,lambda 方法实现就是依赖于该指令;
  • (T)return: 方法退出指令,T 表示返回类型;
    关于 invokespecial 和 invokevirtual 如下图:
    ASM基本概念 - 图5
    invokespecial 和 invokevirtual.png

同步方法指令:

  • monitorenter: 进入并获得对象监视器;
  • monitorexit: 退出并释放对象监视器;