1 class 文件的内容
1.1 class 文件整体结构
java 文件 Test.java(297 B):
package cn.missevan.view;
class Test {
private static final String TAG = "Test";
private int a = 2;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
private void test() {
}
}
编译成字节码,Test.class(421 B),查看二进制代码:
xxd **.class
下面来看下怎么读懂这个二进制文件格式。
首先了解下字节码结构:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
“u” 表示无符号数的含义,u1、u2、u4 分别表示 1 字节、2 字节、4 字节的无符号数。参考下图可以更加清晰地了解整个结构:
图片来自 JVM基础教程第5讲:字节码文件结构。
二进制格式可以节省文件体积,阿里的 VirtualView 框架也是借鉴了 Android 系统编译模板文件的思路使用的 二进制格式,可以参考看看 二进制文件格式,业务里有类似场景也可以借鉴下。
接下来开始针对每个条目进行分析。
1.2 魔数(magic)
用于虚拟机辨别 Class 文件,值为 0xCAFEBABE
。
1.3 minor_version,major_version
minor_version 一般固定为 0 或者 65535(class 文件使用了 jdk 预览功能时)。
major_version 对应关系,我整理了个表格:
JAVA SE | 发布日期 | Major(十进制) | Major(十六进制) |
---|---|---|---|
1.0.2 | 1996.5 | 45 | 0x2D |
1.1 | 1997.2 | 45 | 0x2D |
1.2 | 1998.12 | 46 | 0x2E |
1.3 | 2000.5 | 47 | 0x2F |
1.4 | 2002.2 | 48 | 0x30 |
5.0 | 2004.9 | 49 | 0x31 |
6 | 2006.12 | 50 | 0x32 |
7 | 2011.7 | 51 | 0x33 |
8 | 2014.3 | 52 | 0x34 |
9 | 2017.9 | 53 | 0x35 |
10 | 2018.3 | 54 | 0x36 |
11 | 2018.9 | 55 | 0x37 |
12 | 2019.3 | 56 | 0x38 |
13 | 2019.9 | 57 | 0x39 |
14 | 2020.3 | 58 | 0x3A |
15 | 2020.9 | 59 | 0x3B |
16 | 2021.3 | 60 | 0x3C |
17 | 2021.9 | 61 | 0x3D |
18 | 2022.3 | 62 | 0x3E |
看发布日期可以发现,从 1.9 开始,每年 3 月份和 9 月份都会发布一次新版本,每个新版本 major_version 加 1。
在本文的字节码文件中,minor_version 为 0,major_version 为 003d
,即 java 17。
1.4 constant_pool_count
常量池数量,在开头的字节码中,constant_pool_count 为 001b
,值为 27。注意常量池索引是从 1 开始的(0 是保留值,在某些指向常量池的索引需要表示不引用任何常量的意思时,这个索引就为0),所以要这个值要减一才是真正的常量池数量,也就是 27 - 1 = 26
。
1.5 constant_pool
常量池内容。
常量类型表(我根据最新的 jvm18 文档 画的):
补充说明:
-
CONSTANT_Utf8
的数据长度由length
决定。
字段描述符表示类、实例或局部变量的类型,参考下表。
字段描述符 | 含义 |
---|---|
B | 基本类型 byte,一个字节,-128 ~ 127 |
C | 基本类型 char,两个字节,\u0000 ~ \uFFFF,0 ~ 65535 |
D | 基本类型 double,八个字节 |
F | 基本类型 float,四个字节 |
I | 基本类型 int,四个字节,0x80000000 ~ 0x7fffffff,-2147483648 ~ 2147483647 |
J | 基本类型 long,八个字节,-263 ~ 263-1 |
S | 基本类型 short,两个字节,-32768 ~ 32767 |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 引用类型,以分号结尾,如 Ljava/lang/Object; |
[ | 引用类型,一维数组 |
对照这个表,再回顾下字节码的部分常量池区域。
对着表翻译即可,utf-8 数据内容的参考附录的 ASCII 表,其实终端打印出来的字节码内容,每一行最后面是翻译了一下 utf-8 内容的。翻译完可以看附录的字节码可读格式版本来对比下。
1.6 access_flags
然后紧接着的是该类的访问标志:ACC_SUPER,占两个字节。访问标志的含义如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 pulic,可以被其他包访问 |
ACC_FINAL | 0x0010 | 声明为 final,不允许继承 |
ACC_SUPER | 0x0020 | 对使用 invokespecial 指令调用的父类方法特殊对待 |
ACC_INTERFACE | 0x0200 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型,必须没有初始化 |
ACC_SYNTHETIC | 0x1000 | 编译器自动生成的,源码中不存在。例外:默认的构造函数、类的初始化方法、以及枚举类中的 value 和 valueOf 方法 |
ACC_ANNOTATION | 0x2000 | 声明一个注解接口 |
ACC_ENUM | 0x4000 | 声明一个枚举类 |
ACC_MODULE | 0x8000 | 是模块,不是类或者接口 |
这个表的含义是我根据 官方文档 翻译来的,对 ACC_SYNTHETIC
有所补充,另外 ACC_SUPER
的理解可以参考 ACC_SUPER简介,简单来说就是如果多个父类同时有个重写的方法,会动态查找最近的一个父类方法进行调用。
本文例子中的访问标志是 0020
,代表 ACC_SUPER
,如果有多个标志,则是用或运算得出结果后存储。
1.7 this_class、super_class、interfaces_count、interfaces
接着是当前类索引(2 字节)、父类索引(2 字节)、接口数(2 字节)、接口索引表,根据索引值可以在常量池中找到类的全限定名。本例中的这些信息如下图所示。
1.8 fields_count、fields
接着是字段数(2 字节),字段表。本例中字段数是 0002
。
字段表的结构为:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flags 的定义如下表所示:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 公开的,可以被其他包访问 |
ACC_PRIVATE | 0x0002 | 私有的,只能在定义类和属于同一层级的其他类中访问 |
ACC_PROTECTED | 0x0004 | 受保护的,可以被子类访问 |
ACC_STATIC | 0x0008 | 静态的 |
ACC_FINAL | 0x0010 | final,无法修改 |
ACC_VOLATILE | 0x0040 | volatile |
ACC_TRANSIENT | 0x0080 | 暂时的,不能持久化 |
ACC_SYNTHETIC | 0x1000 | synthetic,编译器自动生成的,源码中不存在 |
ACC_ENUM | 0x4000 | 枚举 |
字段表结构最后是属性表。属性表结构参考 attributes 小节。
本例中的部分字段表字节码如图所示。
1.9 methods_count、methods
再往后是方法数(2 字节),方法表。方法表格式如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flags 的定义如下表所示:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 公开的,可以被其他包访问 |
ACC_PRIVATE | 0x0002 | 私有的,只能在定义类和属于同一层级的其他类中访问 |
ACC_PROTECTED | 0x0004 | 受保护的,可以被子类访问 |
ACC_STATIC | 0x0008 | 静态的 |
ACC_FINAL | 0x0010 | final,无法修改 |
ACC_SYNCHRONIZED | 0x0020 | 同步方法 |
ACC_BRIDGE | 0x0040 | 由编译器生成的桥接方法 |
ACC_VARARGS | 0x0080 | 可变数量的参数声明 |
ACC_NATIVE | 0x0100 | native,用Java编程语言以外的语言实现 |
ACC_ABSTRACT | 0x0400 | 抽象的,没有提供任何实现 |
ACC_STRICT | 0x0800 | 只会出现在 major version 是 46 ~ 60 的类文件中,声明为 strictfp,表示使用 IEEE-754 规范的精确浮点数 |
ACC_SYNTHETIC | 0x1000 | synthetic,编译器自动生成的,源码中不存在 |
本例中的方法表的字节码解析如图所示。
解析到字节码指令的时候,需要对照附录中的指令集表来翻译,每种指令的格式,需要对照 The Java Virtual Machine Instruction Set 查看。
- stack
最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度
- locals:
局部变量所需的存储空间,单位为 Slot, Slot 是虚拟机为局部变量分配内存时所使用的最小单位,为 4 个字节大小。方法参数(包括实例方法中的隐藏参数 this),显示异常处理器的参数( try catch 中的 catch 块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals 的大小并不一定等于所有局部变量所占的 Slot 之和,因为局部变量中的Slot是可以重用的。
- args_size:
方法参数的个数,每个实例方法都会有一个隐藏参数 this
- LineNumberTable
该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none
或 -g:lines
选项来取消或要求生成这项信息,如果选择不生成 LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。
1.10 attributes_count、attributes
最后是属性数(2 字节),属性表,属性表结构如下所示。
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
具体的属性结构参考下表(语雀上写的,掘金对表格内换行支持不太好,这里只能以图片形式贴过来),每个属性开头都是 attribute_name_index、attribute_length,根据 attribute_name 的不同来决定不同的 info 结构。属性非常多,查表建议直接去官网看:4.7. Attributes。
- 内部类 access_flags
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 pulic,可以被其他包访问 |
ACC_PRIVATE | 0x0002 | 私有的,只能在定义类和属于同一层级的其他类中访问 |
ACC_PROTECTED | 0x0004 | 受保护的,可以被子类访问 |
ACC_STATIC | 0x0008 | 静态的 |
ACC_FINAL | 0x0010 | 声明为 final,不允许继承 |
ACC_INTERFACE | 0x0200 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型,必须没有初始化 |
ACC_SYNTHETIC | 0x1000 | 编译器自动生成的,源码中不存在 |
ACC_ANNOTATION | 0x2000 | 声明一个注解接口 |
ACC_ENUM | 0x4000 | 声明一个枚举类 |
2 附录
2.1 ASCII 表
2.2 字节码可读格式
先了解下类分解器 javap
,javap
可以对 class 文件反编译,打印出一个可读版本,我们用到 javap
的主要选项有:
- -public | protected | package | private:用于指定显示哪种级别的类成员,这里注意 -p 是指 private,不加 -p 的话,方法集里不显示私有成员变量和方法。
- -v/-verbose:指定显示更进一步的详细信息。
执行 javap -v -p /Users/jady/Downloads/tmp/Test.class
读取字节码,也可以在 Visual Studio Code 里装一个 JVM Bytecode Viewer 插件,效果如图:
也可以使用 idea 插件 jclasslib 查看,效果如图:
开头的 5 行信息包括: Class 文件当前所在位置,最后修改时间,文件大小,SHA-256 值,编译自哪个文件,类的全限定名。
2.3 指令集
指令集概览:
OPCODE | BYTE | SHORT | INT | LONG | FLOAT | DOUBLE | CHAR | REFERENCE |
---|---|---|---|---|---|---|---|---|
Tipush | bipush | sipush | ||||||
Tconst | iconst | lconst | fconst | dconst | aconst | |||
Tload | iload | lload | fload | dload | aload | |||
T store | istore | lstore | fstore | dstore | astore | |||
Tinc | iinc | |||||||
Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
Tadd | iadd | ladd | fadd | dadd | ||||
Tsub | isub | lsub | fsub | dsub | ||||
Tmul | imul | lmul | fmul | dmul | ||||
Tdiv | idiv | ldiv | fdiv | ddiv | ||||
Trem | irem | Irem | frem | drem | ||||
Tneg | ineg | lneg | fneg | dneg | ||||
Tshl | ishl | Ishl | ||||||
Tshr | ishr | lshr | ||||||
Tushr | iushr | lushr | ||||||
Tand | iand | land | ||||||
Tor | ior | lor | ||||||
Txor | ixor | lxor | ||||||
i2T | i2b | i2s | i2I | i2f | i2d | |||
l2T | 12i | 12f | 12d | |||||
f2T | f2i | f2l | fad | |||||
d2T | d2i | d21 | d2f | |||||
Tcmp | lcmp | |||||||
Tompl | fcmpl | dcmp1 | ||||||
Tcmpg | fcmpg | dcmpg | ||||||
if _TcmpOP | if_jcmpOP | if_acmpOP | ||||||
Treturn | iretum | Ireturn | freturn | dreturn | areturn |
指令码概览:
字节码 | 助记符 | 指令含义 | 字节码 | 助记符 | 指令含义 |
---|---|---|---|---|---|
0x00 | nop | None | 0x65 | lsub | 将栈顶两 long 型数值相减并将结果压入栈顶 |
0x01 | aconst_null | 将 null 推送至栈顶 | 0x66 | fsub | 将栈顶两 float 型数值相减并将结果压入栈顶 |
0x02 | iconst_m1 | 将 int 型-1 推送至栈顶 | 0x67 | dsub | 将栈顶两 double 型数值相减并将结果压入栈顶 |
0x03 | iconst_0 | 将 int 型 0 推送至栈顶 | 0x68 | imul | 将栈顶两 int 型数值相乘并将结果压入栈顶 |
0x04 | iconst_1 | 将 int 型 1 推送至栈顶 | 0x69 | lmul | 将栈顶两 long 型数值相乘并将结果压入栈顶 |
0x05 | iconst_2 | 将 int 型 2 推送至栈顶 | 0x6a | fmul | 将栈顶两 float 型数值相乘并将结果压入栈顶 |
0x06 | iconst_3 | 将 int 型 3 推送至栈顶 | 0x6b | dmul | 将栈顶两 double 型数值相乘并将结果压入栈顶 |
0x07 | iconst_4 | 将 int 型 4 推送至栈顶 | 0x6c | idiv | 将栈顶两 int 型数值相除并将结果压入栈顶 |
0x08 | iconst_5 | 将 int 型 5 推送至栈顶 | 0x6d | ldiv | 将栈顶两 long 型数值相除并将结果压入栈顶 |
0x09 | lconst_0 | 将 long 型 0 推送至栈顶 | 0x6e | fdiv | 将栈顶两 float 型数值相除并将结果压入栈顶 |
0x0a | lconst_1 | 将 long 型 1 推送至栈顶 | 0x6f | ddiv | 将栈顶两 double 型数值相除并将结果压入栈顶 |
0x0b | fconst_0 | 将 float 型 0 推送至栈顶 | 0x70 | irem | 将栈顶两 int 型数值作取模运算并将结果压入栈顶 |
0x0c | fconst_1 | 将 float 型 1 推送至栈顶 | 0x71 | lrem | 将栈顶两 long 型数值作取模运算并将结果压入栈顶 |
0x0d | fconst_2 | 将 float 型 2 推送至栈顶 | 0x72 | frem | 将栈顶两 float 型数值作取模运算并将结果压入栈顶 |
0x0e | dconst_0 | 将 double 型 0 推送至栈顶 | 0x73 | drem | 将栈顶两 double 型数值作取模运算并将结果压入栈顶 |
0x0f | dconst_1 | 将 double 型 1 推送至栈顶 | 0x74 | ineg | 将栈顶 int 型数值取负并将结果压入栈顶 |
0x10 | bipush | 将单字节的常量值 (-128~127) 推送至栈顶 | 0x75 | lneg | 将栈顶 long 型数值取负并将结果压入栈顶 |
0x11 | sipush | 将一个短整型常量 (-32768~32767) 推送至栈顶 | 0x76 | fneg | 将栈顶 float 型数值取负并将结果压入栈顶 |
0x12 | ldc | 将 int,float 或 String 型常量值从常量池中推送至栈顶 | 0x77 | dneg | 将栈顶 double 型数值取负并将结果压入栈顶 |
0x13 | ldc_w | 将 int,float 或 String 型常量值从常量池中推送至栈顶 (宽索引) | 0x78 | ishl | 将 int 型数值左移指定位数并将结果压入栈顶 |
0x14 | ldc2_w | 将 long 或 double 型常量值从常量池中推送至栈顶 (宽索引) | 0x79 | lshl | 将 long 型数值左移指定位数并将结果压入栈顶 |
0x15 | iload | 将指定的 int 型本地变量推送至栈顶 | 0x7a | ishr | 将 int 型数值右 (带符号) 移指定位数并将结果压入栈顶 |
0x16 | lload | 将指定的 long 型本地变量推送至栈顶 | 0x7b | lshr | 将 long 型数值右 (带符号) 移指定位数并将结果压入栈顶 |
0x17 | fload | 将指定的 float 型本地变量推送至栈顶 | 0x7c | iushr | 将 int 型数值右 (无符号) 移指定位数并将结果压入栈顶 |
0x18 | dload | 将指定的 double 型本地变量推送至栈顶 | 0x7d | lushr | 将 long 型数值右 (无符号) 移指定位数并将结果压入栈顶 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶 | 0x7e | iand | 将栈顶两 int 型数值"按位与"并将结果压入栈顶 |
0x1a | iload_0 | 将第一个 int 型本地变量推送至栈顶 | 0x7f | land | 将栈顶两 long 型数值"按位与"并将结果压入栈顶 |
0x1b | iload_1 | 将第二个 int 型本地变量推送至栈顶 | 0x80 | ior | 将栈顶两 int 型数值"按位或"并将结果压入栈顶 |
0x1c | iload_2 | 将第三个 int 型本地变量推送至栈顶 | 0x81 | lor | 将栈顶两 long 型数值"按位或"并将结果压入栈顶 |
0x1d | iload_3 | 将第四个 int 型本地变量推送至栈顶 | 0x82 | ixor | 将栈顶两 int 型数值"按位异或"并将结果压入栈顶 |
0x1e | lload_0 | 将第一个 long 型本地变量推送至栈顶 | 0x83 | lxor | 将栈顶两 long 型数值"按位异或"并将结果压入栈顶 |
0x1f | lload_1 | 将第二个 long 型本地变量推送至栈顶 | 0x84 | iinc | 将指定 int 型变量增加指定值 (如 i++, i–, i+=2 等) |
0x20 | lload_2 | 将第三个 long 型本地变量推送至栈顶 | 0x85 | i2l | 将栈顶 int 型数值强制转换为 long 型数值并将结果压入栈顶 |
0x21 | lload_3 | 将第四个 long 型本地变量推送至栈顶 | 0x86 | i2f | 将栈顶 int 型数值强制转换为 float 型数值并将结果压入栈顶 |
0x22 | fload_0 | 将第一个 float 型本地变量推送至栈顶 | 0x87 | i2d | 将栈顶 int 型数值强制转换为 double 型数值并将结果压入栈顶 |
0x23 | fload_1 | 将第二个 float 型本地变量推送至栈顶 | 0x88 | l2i | 将栈顶 long 型数值强制转换为 int 型数值并将结果压入栈顶 |
0x24 | fload_2 | 将第三个 float 型本地变量推送至栈顶 | 0x89 | l2f | 将栈顶 long 型数值强制转换为 float 型数值并将结果压入栈顶 |
0x25 | fload_3 | 将第四个 float 型本地变量推送至栈顶 | 0x8a | l2d | 将栈顶 long 型数值强制转换为 double 型数值并将结果压入栈顶 |
0x26 | dload_0 | 将第一个 double 型本地变量推送至栈顶 | 0x8b | f2i | 将栈顶 float 型数值强制转换为 int 型数值并将结果压入栈顶 |
0x27 | dload_1 | 将第二个 double 型本地变量推送至栈顶 | 0x8c | f2l | 将栈顶 float 型数值强制转换为 long 型数值并将结果压入栈顶 |
0x28 | dload_2 | 将第三个 double 型本地变量推送至栈顶 | 0x8d | f2d | 将栈顶 float 型数值强制转换为 double 型数值并将结果压入栈顶 |
0x29 | dload_3 | 将第四个 double 型本地变量推送至栈顶 | 0x8e | d2i | 将栈顶 double 型数值强制转换为 int 型数值并将结果压入栈顶 |
0x2a | aload_0 | 将第一个引用类型本地变量推送至栈顶 | 0x8f | d2l | 将栈顶 double 型数值强制转换为 long 型数值并将结果压入栈顶 |
0x2b | aload_1 | 将第二个引用类型本地变量推送至栈顶 | 0x90 | d2f | 将栈顶 double 型数值强制转换为 float 型数值并将结果压入栈顶 |
0x2c | aload_2 | 将第三个引用类型本地变量推送至栈顶 | 0x91 | i2b | 将栈顶 int 型数值强制转换为 byte 型数值并将结果压入栈顶 |
0x2d | aload_3 | 将第四个引用类型本地变量推送至栈顶 | 0x92 | i2c | 将栈顶 int 型数值强制转换为 char 型数值并将结果压入栈顶 |
0x2e | iaload | 将 int 型数组指定索引的值推送至栈顶 | 0x93 | i2s | 将栈顶 int 型数值强制转换为 short 型数值并将结果压入栈顶 |
0x2f | laload | 将 long 型数组指定索引的值推送至栈顶 | 0x94 | lcmp | 比较栈顶两 long 型数值大小, 并将结果 (1, 0 或-1) 压入栈顶 |
0x30 | faload | 将 float 型数组指定索引的值推送至栈顶 | 0x95 | fcmpl | 比较栈顶两 float 型数值大小, 并将结果 (1, 0 或-1) 压入栈顶;当其中一个数值为 NaN 时, 将-1 压入栈顶 |
0x31 | daload | 将 double 型数组指定索引的值推送至栈顶 | 0x96 | fcmpg | 比较栈顶两 float 型数值大小, 并将结果 (1, 0 或-1) 压入栈顶;当其中一个数值为 NaN 时, 将 1 压入栈顶 |
0x32 | aaload | 将引用类型数组指定索引的值推送至栈顶 | 0x97 | dcmpl | 比较栈顶两 double 型数值大小, 并将结果 (1, 0 或-1) 压入栈顶;当其中一个数值为 NaN 时, 将-1 压入栈顶 |
0x33 | baload | 将 boolean 或 byte 型数组指定索引的值推送至栈顶 | 0x98 | dcmpg | 比较栈顶两 double 型数值大小, 并将结果 (1, 0 或-1) 压入栈顶;当其中一个数值为 NaN 时, 将 1 压入栈顶 |
0x34 | caload | 将 char 型数组指定索引的值推送至栈顶 | 0x99 | ifeq | 当栈顶 int 型数值等于 0 时跳转 |
0x35 | saload | 将 short 型数组指定索引的值推送至栈顶 | 0x9a | ifne | 当栈顶 int 型数值不等于 0 时跳转 |
0x36 | istore | 将栈顶 int 型数值存入指定本地变量 | 0x9b | iflt | 当栈顶 int 型数值小于 0 时跳转 |
0x37 | lstore | 将栈顶 long 型数值存入指定本地变量 | 0x9c | ifge | 当栈顶 int 型数值大于等于 0 时跳转 |
0x38 | fstore | 将栈顶 float 型数值存入指定本地变量 | 0x9d | ifgt | 当栈顶 int 型数值大于 0 时跳转 |
0x39 | dstore | 将栈顶 double 型数值存入指定本地变量 | 0x9e | ifle | 当栈顶 int 型数值小于等于 0 时跳转 |
0x3a | astore | 将栈顶引用类型数值存入指定本地变量 | 0x9f | if_icmpeq | 比较栈顶两 int 型数值大小, 当结果等于 0 时跳转 |
0x3b | istore_0 | 将栈顶 int 型数值存入第一个本地变量 | 0xa0 | if_icmpne | 比较栈顶两 int 型数值大小, 当结果不等于 0 时跳转 |
0x3c | istore_1 | 将栈顶 int 型数值存入第二个本地变量 | 0xa1 | if_icmplt | 比较栈顶两 int 型数值大小, 当结果小于 0 时跳转 |
0x3d | istore_2 | 将栈顶 int 型数值存入第三个本地变量 | 0xa2 | if_icmpge | 比较栈顶两 int 型数值大小, 当结果大于等于 0 时跳转 |
0x3e | istore_3 | 将栈顶 int 型数值存入第四个本地变量 | 0xa3 | if_icmpgt | 比较栈顶两 int 型数值大小, 当结果大于 0 时跳转 |
0x3f | lstore_0 | 将栈顶 long 型数值存入第一个本地变量 | 0xa4 | if_icmple | 比较栈顶两 int 型数值大小, 当结果小于等于 0 时跳转 |
0x40 | lstore_1 | 将栈顶 long 型数值存入第二个本地变量 | 0xa5 | if_acmpeq | 比较栈顶两引用型数值, 当结果相等时跳转 |
0x41 | lstore_2 | 将栈顶 long 型数值存入第三个本地变量 | 0xa6 | if_acmpne | 比较栈顶两引用型数值, 当结果不相等时跳转 |
0x42 | lstore_3 | 将栈顶 long 型数值存入第四个本地变量 | 0xa7 | goto | 无条件跳转 |
0x43 | fstore_0 | 将栈顶 float 型数值存入第一个本地变量 | 0xa8 | jsr | 跳转至指定的 16 位 offset 位置, 并将 jsr 的下一条指令地址压入栈顶 |
0x44 | fstore_1 | 将栈顶 float 型数值存入第二个本地变量 | 0xa9 | ret | 返回至本地变量指定的 index 的指令位置 (一般与 jsr 或 jsr_w 联合使用) |
0x45 | fstore_2 | 将栈顶 float 型数值存入第三个本地变量 | 0xaa | tableswitch | 用于 switch 条件跳转, case 值连续 (可变长度指令) |
0x46 | fstore_3 | 将栈顶 float 型数值存入第四个本地变量 | 0xab | lookupswitch | 用于 switch 条件跳转, case 值不连续 (可变长度指令) |
0x47 | dstore_0 | 将栈顶 double 型数值存入第一个本地变量 | 0xac | ireturn | 从当前方法返回 int |
0x48 | dstore_1 | 将栈顶 double 型数值存入第二个本地变量 | 0xad | lreturn | 从当前方法返回 long |
0x49 | dstore_2 | 将栈顶 double 型数值存入第三个本地变量 | 0xae | freturn | 从当前方法返回 float |
0x4a | dstore_3 | 将栈顶 double 型数值存入第四个本地变量 | 0xaf | dreturn | 从当前方法返回 double |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量 | 0xb0 | areturn | 从当前方法返回对象引用 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量 | 0xb1 | return | 从当前方法返回 void |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量 | 0xb2 | getstatic | 获取指定类的静态域, 并将其压入栈顶 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量 | 0xb3 | putstatic | 为指定类的静态域赋值 |
0x4f | iastore | 将栈顶 int 型数值存入指定数组的指定索引位置 | 0xb4 | getfield | 获取指定类的实例域, 并将其压入栈顶。另有两字节参数。 |
0x50 | lastore | 将栈顶 long 型数值存入指定数组的指定索引位置 | 0xb5 | putfield | 为指定类的实例域赋值 |
0x51 | fastore | 将栈顶 float 型数值存入指定数组的指定索引位置 | 0xb6 | invokevirtual | 调用实例方法。另有两字节参数。 |
0x52 | dastore | 将栈顶 double 型数值存入指定数组的指定索引位置 | 0xb7 | invokespecial | 调用父类构造方法, 实例初始化方法, 私有方法。另有两字节参数。 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 | 0xb8 | invokestatic | 调用静态方法 |
0x54 | bastore | 将栈顶 boolean 或 byte 型数值存入指定数组的指定索引位置 | 0xb9 | invokeinterface | 调用接口方法 |
0x55 | castore | 将栈顶 char 型数值存入指定数组的指定索引位置 | 0xba | invokedynamic | 调用动态方法 |
0x56 | sastore | 将栈顶 short 型数值存入指定数组的指定索引位置 | 0xbb | new | 创建一个对象, 并将其引用引用值压入栈顶 |
0x57 | pop | 将栈顶数值弹出 (数值不能是 long 或 double 类型的) | 0xbc | newarray | 创建一个指定的原始类型 (如 int, float, char 等) 的数组,并将其引用值压入栈顶 |
0x58 | pop2 | 将栈顶的一个 (对于非 long 或 double 类型) 或两个数值 (对于非 long 或 double 的其他类型) 弹出 | 0xbd | anewarray | 创建一个引用型 (如类, 接口, 数组) 的数组, 并将其引用值压入栈顶 |
0x59 | dup | 复制栈顶数值并将复制值压入栈顶 | 0xbe | arraylength | 获取数组的长度值并压入栈顶 |
0x5a | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 | 0xbf | athrow | 将栈顶的异常抛出 |
0x5b | dup_x2 | 复制栈顶数值并将三个 (或两个) 复制值压入栈顶 | 0xc0 | checkcast | 检验类型转换, 检验未通过将抛出 ClassCastException |
0x5c | dup2 | 复制栈顶一个 (对于 long 或 double 类型) 或两个 (对于非 long 或 double 的其他类型) 数值并将复制值压入栈顶 | 0xc1 | instanceof | 检验对象是否是指定类的实际, 如果是将 1 压入栈顶, 否则将 0 压入栈顶 |
0x5d | dup2_x1 | dup_x1 指令的双倍版本 | 0xc2 | monitorenter | 获得对象的锁, 用于同步方法或同步块 |
0x5e | dup2_x2 | dup_x2 指令的双倍版本 | 0xc3 | monitorexit | 释放对象的锁, 用于同步方法或同步块 |
0x5f | swap | 将栈顶最顶端的两个数值互换 (数值不能是 long 或 double 类型) | 0xc4 | wide | 扩展本地变量的宽度 |
0x60 | iadd | 将栈顶两 int 型数值相加并将结果压入栈顶 | 0xc5 | multianewarray | 创建指定类型和指定维度的多维数组 (执行该指令时,操作栈中必须包含各维度的长度值), 并将其引用压入栈顶 |
0x61 | ladd | 将栈顶两 long 型数值相加并将结果压入栈顶 | 0xc6 | ifnull | 为 null 时跳转 |
0x62 | fadd | 将栈顶两 float 型数值相加并将结果压入栈顶 | 0xc7 | ifnonnull | 不为 null 时跳转 |
0x63 | dadd | 将栈顶两 double 型数值相加并将结果压入栈顶 | 0xc8 | goto_w | 无条件跳转 (宽索引) |
0x64 | isub | 将栈顶两 int 型数值相减并将结果压入栈顶 | 0xc9 | jsr_w | 跳转至指定的 32 位 offset 位置, 并将 jsr_w 的下一条指令地址压入栈顶 |
字节码指令:
加载和存储指令助记符:
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
load将一个局部变量加载到操作栈:iload、lload、fload、aload等。
store将一个数值从操作数栈存储到局部变量表:istore、lstore、fstore等。
将一个常量加载到操作数栈:bipush、sipush、ldc等
运算指令助记符:
加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
等等。
类型转换指令:
Java虚拟机直接支持以下数值类型的宽化类型转换:
int类型到long、float或者double
long类型到float、double
float类型到double
相对的,处理窄化类型转换时,必须显示的使用转换指令来完成,这些转换指令包括:i2b、i2c、l2i、f2i、d2i等等。
对象创建与访问指令:
创建类实例的指令:new
创建数组的指令:newaray、anewarray
访问类字段(static字段)和实例字段(非static字段)的指令:getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload等等
将操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore等等
查数组长度的指令:arraylength
检查实例类型的指令:instanceof、checkcast
操作数栈管理指令:
将操作数栈顶一个或两个元素出栈:pop、pop2
复制栈顶一个或两个数值将复制值或者双份的复制值重新压如栈顶:dup、dup2
将栈最顶端的两个数值互换:swap
控制转移指令:
控制转移指令可以让Java虚拟机有条件或无条件的从指定的位置执行而不是控制转移指令的下一条指令继续执行程序,
可以认为控制转移指令就是在有条件或无条件的修改PC寄存器的值。
条件分支:ifeq、ifnull等
无条件分支:goto、goto_w、jsr、ret
方法调用和返回指令:
invokevirtual指令:用于调用对象的实例方法。
invokeinterface指令:用于调用接口方法,他会在运行时搜索一个实现了这个接口的对象,找出适合的方法进行调用。
invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令:用于调用类方法(static方法)。
异常处理指令:
在Java程序中显示抛出异常的操作(throw)都有athrow指令来实现,除了用throw语句显示抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在整数运算中,当除数为零时,虚拟机会抛出ArithmeticException异常。在Java虚拟机中,处理异常(catch)不是由字节码来实现的,而是采用异常表(Code属性中)完成的。
3 参考文档
[1] oracle. JVM 虚拟机规范
[2] oracle. The class File Format
[3] CoderLi. Java synthetic
[4] 陈树义. JVM基础教程第5讲:字节码文件结构
[5] 美团技术团队. 字节码增强技术探索
[6] zhz小白. JVM——(19)字节码指令集与解析一(局部变量压栈、常量变量压栈、出栈局部变量表指令)
网友评论