最近在研究ASM 字节码增强技术,要掌握ASM 必须要先连接Java字节码结构、JVM栈帧和常用JVM指令。
本章就Java字节码和JVM栈帧和JVM指令做一些梳理和整理加以沉淀,方便将来复习查阅。
一、ByteCode字节码
1.1、字节码码结构
Java代码最终都会被编译成class文件(字节码),这些字节码可以被JVM正确识别和解析。
JVM 对java字节码有严格的规定,如下图所示:
字节码结构图.png
字节码结构示意图
- Magic: 该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
- Version: 该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
- Constant Pool: 该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
- Access_flag: 该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
- This Class: 指向表示该类全限定名称的字符串常量的指针。
- Super Class: 指向表示父类全限定名称的字符串常量的指针。
- Interfaces: 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。
- Fields: 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
- Fields: 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
- Class attributes: 该项存放了在该文件中类或接口所定义的属性的基本信息。
1.2、举个栗子
以TestCost 为例
package com.sogou.iot.trplugin;
/**
* 文件名:TestCost
* 创建者:baixuefei
* 创建日期:2021/1/21 9:52 AM
* 职责描述:
*/
class TestCost {
public int add(int a,int b) {
return a+b;
}
}
生成字节码
javac -g TestCost.java
-g 参数表示生成所有调试信息
生成TestCoast.class文件
查看字节码的信息
javap -verbose TestCost.class
-verbose 表示输出输出附加信息
TestCost.class 解析后的信息
Classfile /Users/feifei/Desktop/TM/Demo/TrPlugin/app/src/main/java/com/sogou/iot/trplugin/TestCost.class
Last modified 2021-1-24; size 401 bytes
MD5 checksum 684202ea0bfc50a7d404275086566aaf
Compiled from "TestCost.java"
class com.sogou.iot.trplugin.TestCost
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #3.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // com/sogou/iot/trplugin/TestCost
#3 = Class #20 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/sogou/iot/trplugin/TestCost;
#11 = Utf8 add
#12 = Utf8 (II)I
#13 = Utf8 a
#14 = Utf8 I
#15 = Utf8 b
#16 = Utf8 SourceFile
#17 = Utf8 TestCost.java
#18 = NameAndType #4:#5 // "<init>":()V
#19 = Utf8 com/sogou/iot/trplugin/TestCost
#20 = Utf8 java/lang/Object
{
com.sogou.iot.trplugin.TestCost();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sogou/iot/trplugin/TestCost;
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lcom/sogou/iot/trplugin/TestCost;
0 4 1 a I
0 4 2 b I
}
SourceFile: "TestCost.java"
TestCost.class文件如何解读呢?这还需要先了解JVM的栈帧结构
1.3、JVM栈帧
JVM中每调用一个方法 就会生成一个栈帧。栈帧中包含的内容包括:局部变量表、操作栈、动态链接、和方法返回地址四部分。
image
栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束。
栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表(局部变量表)、操作数栈和指向当前方法所属的类的运行时常量池的引用。
1.3.1、局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。
在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
public int add(int a,int b) {
return a+b;
}
以add方法为例,其局部变量有几个?a、b分别算一个,类的实例方法通常还有有一个隐藏的参数就是this。所以add方法有三个参数,分别是this,a,b。
所以add方法堆栈对应的局部变量表大小为3,如LocalVariableTable所示:
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lcom/sogou/iot/trplugin/TestCost;
0 4 1 a I
0 4 2 b I
1.3.2、操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
JVM指令
- load 命令:用于将局部变量表的指定位置的相应类型变量加载到操作数栈顶;
- store命令:用于将操作数栈顶的相应类型数据保入局部变量表的指定位置;
- invokevirtual:调用实例方法
- ireturn: 当前方法返回int
a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示
image
image
1.3.3、动态连接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池.
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
1.3.4、方法返回
当一个方法开始执行时,可能有两种方式退出该方法:正常完成出口和异常完成出口。
- 正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。
- 异常完成出口 是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
二、常用的JVM指令
2.1、栈操作相关
load和store
- load 命令:用于将局部变量表的指定位置的相应类型变量加载到栈顶;
- store命令:用于将栈顶的相应类型数据保入局部变量表的指定位置;
| 变量进栈 | 含义 | 变量保存 | 含义 | |
|---|---|---|---|---|
| iload | 第1个int型变量进栈 | istore | 栈顶int数值存入第1局部变量 | |
| iload_0 | 第1个int型变量进栈 | istore_0 | 栈顶int数值存入第1局部变量 | |
| iload_1 | 第2个int型变量进栈 | istore_1 | 栈顶int数值存入第2局部变量 | |
| iload_2 | 第3个int型变量进栈 | istore_2 | 栈顶int数值存入第3局部变量 | |
| iload_3 | 第4个int型变量进栈 | istore_3 | 栈顶int数值存入第4局部变量 | |
| lload | 第1个long型变量进栈 | lstore | 栈顶long数值存入第1局部变量 | |
| fload | 第1个float型变量进栈 | fstore | 栈顶float数值存入第1局部变量 | |
| dload | 第1个double型变量进栈 | dstore | 栈顶double数值存入第1局部变量 | |
| aload | 第1个ref型变量进栈 | astore | 栈顶ref对象存入第1局部变量 |
const、push和ldc
- const、push:将相应类型的常量放入栈顶
- ldc:则是从常量池中将常量压入操作数栈栈顶
| 变量进栈 | 含义 | 变量保存 | 含义 |
|---|---|---|---|
| iload | 第1个int型变量进栈 | istore | 栈顶int数值存入第1局部变量 |
| iload_0 | 第1个int型变量进栈 | istore_0 | 栈顶int数值存入第1局部变量 |
| iload_1 | 第2个int型变量进栈 | istore_1 | 栈顶int数值存入第2局部变量 |
| iload_2 | 第3个int型变量进栈 | istore_2 | 栈顶int数值存入第3局部变量 |
| iload_3 | 第4个int型变量进栈 | istore_3 | 栈顶int数值存入第4局部变量 |
| lload | 第1个long型变量进栈 | lstore | 栈顶long数值存入第1局部变量 |
| fload | 第1个float型变量进栈 | fstore | 栈顶float数值存入第1局部变量 |
| dload | 第1个double型变量进栈 | dstore | 栈顶double数值存入第1局部变量 |
| aload | 第1个ref型变量进栈 | astore | 栈顶ref对象存入第1局部变量 |
| 常量池操作 | 含义 |
|---|---|
| ldc | int、float或String型常量从常量池推送至栈顶 |
| ldc_w | int、float或String型常量从常量池推送至栈顶(宽索引) |
| ldc2_w | long或double型常量从常量池推送至栈顶(宽索引) |
pop和dup
- pop用于栈顶数值出栈操作;
- dup用于复制栈顶的指定个数的数值,并将其压入栈顶指定次数
| 栈顶操作 | 含义 |
|---|---|
| pop | 栈顶数值出栈(不能是long/double) |
| pop2 | 栈顶数值出栈(long/double型1个,其他2个) |
| dup | 复制栈顶数值,并压入栈顶 |
| dup_x1 | 复制栈顶数值,并压入栈顶2次 |
| dup_x2 | 复制栈顶数值,并压入栈顶3次 |
| dup2 | 复制栈顶2个数值,并压入栈顶 |
| dup2_x1 | 复制栈顶2个数值,并压入栈顶2次 |
| dup2_x2 | 复制栈顶2个数值,并压入栈顶3次 |
| swap | 栈顶的两个数值互换,且不能是long/double |
dup2对于long、double类型的数据就是一个,对于其他类型的数据,才是真正的两个,这个的2代表的是2个slot的数据。
2.2、对象相关
字段调用
| 字段调用 | 含义 |
|---|---|
| getstatic | 获取类的静态字段,将其值压入栈顶 |
| putstatic | 给类的静态字段赋值 |
| getfield | 获取对象的字段,将其值压入栈顶 |
| putfield | 给对象的字段赋值 |
方法调用
| 方法调用 | 作用 | 解释 |
|---|---|---|
| invokevirtual | 调用实例方法 | 虚方法分派 |
| invokestatic | 调用类方法 | static方法 |
| invokeinterface | 调用接口方法 | 运行时搜索合适方法调用 |
| invokespecial | 调用特殊实例方法 | 包括实例初始化方法、父类方法 |
| invokedynamic | 由用户引导方法决定 | 运行时动态解析出调用点限定符所引用方法 |
方法返回
| 方法返回 | 含义 | 解释 |
|---|---|---|
| ireturn | 当前方法返回int | 虚方法分派 |
| lreturn | 当前方法返回long | static方法 |
| freturn | 当前方法返回float | 运行时搜索合适方法调用 |
| dreturn | 当前方法返回double | 包括实例初始化方法、父类方法 |
| areturn | 当前方法返回ref | 运行时动态解析出调用点限定符所引用方法 |
对象和数组
- 创建类实例: new
- 创建数组:newarray、anewarray、multianewarray
- 数组元素 加载到 操作数栈:xaload (x可为b,c,s,i,l,f,d,a)
- 操作数栈的值 存储到数组元素: xastore (x可为b,c,s,i,l,f,d,a)
- 数组长度:arraylength
- 类实例类型:instanceof、checkcast
2.3、运算指令
运算指令是用于对操作数栈上的两个数值进行某种运算,并把结果重新存入到操作栈顶
| 运算 | int | long | float | double |
|---|---|---|---|---|
| 加法 | 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
- 自增:iin
- 比较:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
2.4、类型转换
类型转换用于将两种不同类型的数值进行转换。
类型转换指令:i2b, i2c,f2i等等。
2.5、流程控制
控制指令是指有条件或无条件地修改PC寄存器的值,从而达到控制流程的目标
- 条件分支:ifeq、iflt、ifnull、ifnonnull等
- 复合分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
2.6、同步与异常
- Java程序显式抛出异常: athrow指令。
- Java虚拟机的指令集中通过monitorenter和monitorexit两条指令来完成synchronized的功能
三、参考文章
http://gityuan.com/2015/10/24/jvm-bytecode-grammar/
https://zhuanlan.zhihu.com/p/45354152
https://zhuanlan.zhihu.com/p/94498015?utm_source=wechat_timeline











网友评论