Java 字节码文件查看
- package bytecode;
- public class Test01 {
- private int i = 0;
- public int getI() {
- return i;
- }
- public void setI(int i) {
- this.i = i;
- }
- }
编译这个类, 得到 Test01.class 文件
IDE 查看
用 IDEA 编译器查看
我们发现查看到的 class 文件与类文件基本相同, 这是因为 IDE 自带的 Fernflower decompiler 将字节码文件反编译的结果. 我们可以在插件市场查找安装 jclasslib 插件, 来在 IDEA 中查看 class 文件.
hexedit 查看
通过 hexedit 直接查看该字节码文件
扩展: hexedit 安装
输入: sudo apt install hexedit
当你启动它时, 你必须指定要打开的文件的位置, 然后它会为你打开它.
javap -verbose 查看
通过 javap 指令查看字节码文件: javap -verbose ****
执行 javap -verbose 指令, 得到的结果如下:
- Classfile /home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/bytecode/Test01.class
- Last modified 2019-12-3; size 460 bytes
- MD5 checksum 7913e827b66fbb2c05907b76dafa32ec
- Compiled from "Test01.java"
- public class bytecode.Test01
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #4.#20 // java/lang/Object."<init>":()V
- #2 = Fieldref #3.#21 // bytecode/Test01.i:I
- #3 = Class #22 // bytecode/Test01
- #4 = Class #23 // java/lang/Object
- #5 = Utf8 i
- #6 = Utf8 I
- #7 = Utf8 <init>
- #8 = Utf8 ()V
- #9 = Utf8 Code
- #10 = Utf8 LineNumberTable
- #11 = Utf8 LocalVariableTable
- #12 = Utf8 this
- #13 = Utf8 Lbytecode/Test01;
- #14 = Utf8 getI
- #15 = Utf8 ()I
- #16 = Utf8 setI
- #17 = Utf8 (I)V
- #18 = Utf8 SourceFile
- #19 = Utf8 Test01.java
- #20 = NameAndType #7:#8 // "<init>":()V
- #21 = NameAndType #5:#6 // i:I
- #22 = Utf8 bytecode/Test01
- #23 = Utf8 java/lang/Object
- {
- public bytecode.Test01();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: aload_0
- 5: iconst_0
- 6: putfield #2 // Field i:I
- 9: return
- LineNumberTable:
- line 7: 0
- line 8: 4
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 10 0 this Lbytecode/Test01;
- public int getI();
- descriptor: ()I
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: getfield #2 // Field i:I
- 4: ireturn
- LineNumberTable:
- line 11: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 5 0 this Lbytecode/Test01;
- public void setI(int);
- descriptor: (I)V
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=2, args_size=2
- 0: aload_0
- 1: iload_1
- 2: putfield #2 // Field i:I
- 5: return
- LineNumberTable:
- line 15: 0
- line 16: 5
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 6 0 this Lbytecode/Test01;
- 0 6 1 i I
- }
- SourceFile: "Test01.java"
使用 javap -verbose 命令分析一个字节码文件时, 将会分析该字节码文件的魔数, 版本号, 常量池, 类信息, 类的构造方法, 类中的方法信息, 类变量与成员变量等信息.
Java 字节码文件结构剖析
Class 字节码中只有两种数据类型:
字节数据直接量: 属于基本的数据类型, 以 u1,u2,u4,u8 来分别代表 1,2,4,8 个字节组成的整体数据. 可以用来描述数字, 索引引用, 数量值, 或者按照 UTF-8 编码的字符串值.
表(数组): 由多个基本元素或者其他表, 按照既定顺序组成的大的数据集合. 表是有结构的, 它的结构体现在组成表的成分所在的位置和顺序都是已经严格定义好的.
魔数(magic)
所有的. class 字节码文件的前 4 个字节都是魔数, 魔数值为固定值: 0xCAFEBABE
版本号(version)
魔数之后的 4 个字节是版本信息, 前 2 个字节表示次版本号(minor version), 后两个字节为主版本号(major version). 这里的版本号为 0x 00 00 00 34, 表示次版本号为 0, 主版本号为 52(这里的 52 值的其实是 jdk8, 如果是 jdk7 的话就是 51). 与指令返回的版本号一致.
- minor version: 0
- major version: 52
常量池(Constant pool)
紧接着主版本号之后的, 就是常量池入口. 一个 Java 类定义的很多信息都是由常量池来维护和描述的, 可以将常量池看作是 class 文件的资源仓库, 比如说 Java 类中定义的方法与变量信息, 都是存储在常量池中.
常量池中主要两类常量:
字面量: 如文本字符串, Java 中声明为 final 的常量值等
符号引用: 如类和接口的全局限定名, 字段的名称和描述符, 方法的名称和描述符等
常量池的总体结构:
Java 类所对应的常量池主要由常量池数量与常量池表这两部分组成:
常量池数量紧跟在主版本号之后, 占据两个字节
常量池表则紧跟在常量池数量之后
常量池表与一般的数组不同的是, 常量池表中不同的元素的类型, 结构和长度都是不同的; 但是, 每一种元素的第一个数据都是一个 u1 类型, 该字节是个标识位, 占据一个字节. JVM 在解析产量池时, 会根据这个 u1 类型来获取元素的具体类型.
值得注意的是: 常量池表中元素个数 = 常量池数量 - 1. 其中 0 暂不使用, 目的是满足某些常量池索引值的数据在特定情况下表达[不引用任何常量池] 的含义; 根本原因在于索引 0 也是一个常量(保留常量), 只不过它不位于常量表, 这个常量就对应 null 值, 所以常量池的索引是从 1 开始的. 在本例中, 0x 00 18 代表常量池数量, 常量池数量为 24 - 1 = 23 个, 与 javap 的结果相同.
目前, 常量池中出现的常量类型有 14 种, 如下表:
后面三种都是 Java7 之后出现的, 关于动态引用的. 我们主要看前面 11 种, 下表给出了前 11 个常量类型的详细结构说明:
有了这两张表就可以继续剖析常量池的内容了. 每一个常量的第一个字节都是标志位, 第一个常量的标志为 0x 0A, 转换为十进制为 10, 表示常量类型为: CONSTANT_Methodref_info, 按照上表, 第一个 index 为 2 个字节, 第二 index 也为 2 个字节, 0x 0A 00 04 00 14 这五个字节表示常量池中第一个常量, 第一个 index 值为 4, 第二个 index 值为 20, 与 javap 反编译的结果一致:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V.
指向声明字段的类或者接口描述符 CONSTANT_Class_info 的索引项为 4,4 对应的是:
#4 = Class #23 // java/lang/Object
指向字段描述符的 CONSTANT_NameAndType_info 的索引项为 20,20 对应的是:
- #20 = NameAndType #7:#8 // "<init>":()V, 这个又指向 7 和 8:
- #7 = Utf8 <init>
- #8 = Utf8 ()V
- PS:
在 JVM 规范中, 每个变量, 字段都有描述信息, 描述信息主要的作用是描述字段的数据类型, 方法的参数列表 (包括数量, 类型与顺序) 与返回值. 根据描述符规则, 基本数据类型和的代表无返回值的 void 类用都用一个大写字符表示, 对象类型则使用大写字符 L 加对象的全限定名称来表示. 为了压缩字节码文件的体积, JVM 都只使用一个大写字母表示, 如下所示:
- B - byte
- C - char
- D - double
- F - float
- I - int
- J - Long
- Z - boolean
- V - void
L - 对象类型, 如 Ljava/lang/String;
对于数组类型来说, 每一个维度使用一个前置的 [来表示, 如 int[] 被记录为:[I,String[][]被记录为:[[Ljava/lang/String;
用描述符描述方法时, 按照先参数列表, 后返回值的顺序来描叙. 参数列表按照参数的严格顺序放在一组 () 之内, 如方法: String test( int id, String name)的描述符为:(I, Ljava/lang/String;)Ljava/lang/String;
字节码访问标识(access_flags)
紧跟着常量池的是字节码访问标识, 占据两个字节. 访问标志信息包括该 Class 文件是类还是接口, 是否被定义为 public, 是否是 abstract, 如果是类, 是否被声明成 final.
本文例中 0x 00 21 是 0x 0020 与 0x0001 的并集, 表示 ACC_PUBLIC 与 ACC_SUPER.
类索引与父类索引
紧跟着字节码访问标识的是类索引和父类索引, 分别占据两个字节.
本例中, 0x 00 03 表示类索引, 在常量池中对应:#3 = Class #22 // bytecode/Test01
0x 00 04 表示父类索引, 在常量池中对应:#4 = Class #23 // java/lang/Object
接口(interfaces)
紧跟着父类索引的是接口, 接口由两部分组成:
接口个数: 接口个数占据两个字节, 本例中, 接口个数为 0x 00 00
接口名: 接口个数为 0 时, 接口名不会出现, 如果接口个数大于等于一, 接口名才会出现, 占据两个字节
字段(fields)
紧跟着接口的是字段, 字段用于描述类和接口中声明的变量. 这里的字段包含了类级别变量以及实例变量, 但是不包括方法内部声明的局部变量.
字段由两部分组成:
字段个数: 字段个数占据两个字节, 本文例中为 0x 00 01, 表示只有一个字段
字段表: 字段表由 4 部分组成:
access_flags: 访问标识, 占据两个字节, 本文例中为 0x 00 02, 表示为 ACC_PRIVATE
name_index: 名字, 占据两个字节, 本文例中为 0x 00 05, 对应值为:** #5 = Utf8 i**
descriptor_index: 描述符, 占据两个字节, 本文例中为 0x 00 06, 对应值为:** #6 = Utf8 I**
attributes_count: 属性个数, 占据两个字节, 本文例中为 0x 00 00
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法(method)
紧跟着字段的是方法, 方法由两部分组成:
方法个数: 方法个数占据 2 个字节, 本例中为 0x 00 03, 表示方法表中将由三个方法
方法表: 方法表的结构如下:
方法表
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法中的每个属性都是一个 attribute_info 结构, 方法的属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info[attribute_length] | 1 |
JVM 预定义了部分的 attribute, 但是编译器自己也可以实现自己的 attribute 写入 class 文件里, 供运行时使用. 不同的 attribute 通过 attribute_name_index 来区分.
Code 结构
Code attribute 的作用是保存该方法的结构, 如所对应的字节码:
- Code_attribute {
- u2 attribute_name_index;
- u4 attribute_length;
- u2 max_stack;
- u2 max_locals;
- u4 code_length;
- u1 code[code_length];
- u2 exception_table_length;
- {
- u2 start_pc;
- u2 end_pc;
- u2 handler_pc;
- u2 catch_type;
- } exception_table[exception_table_length];
- u2 attributes_count;
- attribute_info attributes[attributes_count];
- }
attribute_name_index: 属性名索引
attribute_length: 表示 attribute 属性所包含的字节数, 不包含 attribute_name_index 和 attribute_length 字段
max_stack: 表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
max_locals: 表示方法执行期间创建的局部变量的数目, 包含用来表达传入的参数的局部变量
code_length: 表示该方法所包含的字节码的字节数以及具体的指令码
code[code_length]: 具体的指令码, 是该方法被调用时, 虚拟机所执行的字节码
exception_table: 存放的是处理异常的信息, 每个 exception_table 表项由 start_pc,end_pc,handler_pc,catch_type 组成
start_pc 和 end_pc 表示在 code 数组中的从 start_pc 和 end_pc 处 (包含 start_pc, 不包含 end_pc) 的指令抛出的异常会由这个表项来处理
handler_pc 表示处理异常的代码的开始处
catch_type 表示会被处理的异常类型, 它指常量池中的一个异常类. 当 catch_type 为 0 时, 表示处理所有的异常
attributes_count: 属性值
行号表(LineNumberTable): 这个属性用来表示 code 数组的字节码和 Java 代码行数之间的关系, 可以用来在调试的时候定位代码执行的行数
- LineNumberTable_attribute {
- u2 attribute_name_index;
- u4 attribute_length;
- u2 line_number_table_length;
- {
- u2 start_pc;
- u2 line_number;
- }line_number_table[line_number_table_length];
- }
局部变量表(LocalVariableTable):
- LocalVariableTable_attribute {
- u2 attribute_name_index;
- u4 attribute_length; // 不包括起始 6 个字节的属性长度.
- u2 local_variable_table_length; // local_variable_table 表中的项数.
- {
- u2 start_pc;
- u2 length;
- u2 name_index;
- u2 descriptor_index;
- u2 index;
- } local_variable_table[local_variable_table_length];
- }
本文例字节码方法解读
如图所示, 方法从 0x 00 03, 表示本字节码文件由三个方法, 这里将会仔细解读第一个方法:
0x 00 01:access_flags, 对应值为 ACC_PUBLIC
0x 00 07:name_index, 对应常量池中的值为:
0x 00 08:descriptor_index, 对应常量池中的值为:()V
0x 00 01:attributes_count, 表示这个方法有一个属性值
0x 00 09:attribute_name_index, 对应常量池中的值为: Code, 表示这是一个 Code attribute
0x 00 00 00 38:attribute_length, 属性表长度, 表示这个属性的长度为 56 个字节
- 0x 00 02:max_stack
- 0x 00 01:max_locals
0x 00 00 00 0A:code_length, 表示 code 的长度为 10 个字节
- 0x 2A B7 00 01 2A 03 B5 00 02 B1:code[code_length]
- 0x 2A:aload_0
- 0x B7 00 01:invokespecial #1
- 0x 2A:aload_0
- 0x 03:iconst_0
- 0x B5 00 02:putfield #2
- 0x B1:return
- 0x 00 00:exception_table_length
0x 00 02:attributes_count, 表示该 Code attribute 有两个属性
0x 00 0A:attribute_name_index, 对应常量池中的值为: LineNumberTable
0x 00 00 00 0A:attribute_length, 表示这个行号表的长度为 10
0x 00 02:line_number_table_length, 表示两对映射
0x 00 00 00 07: 字节码偏移量为 0, 映射到源代码的行号为 7
0x 00 04 00 08: 字节码偏移量为 4, 映射到源代码的行号为 8
0x 00 0B:attribute_name_index, 对应常量池中的值为: LocalVariableTable
0x 00 00 00 0C:attribute_length, 表示这个行号表的长度为 12
0x 00 01: 局部变量的个数为 1
0x 00 00 00 0A: 开始位置是 0, 结束位置是 10
0x 00 0C: 局部变量的值, 对应常量池的值为: this
0x 00 0D: 局部变量的描述, 对应常量池的值为: Lbytecode/Test01;
0x 00 00: 索引值
至此, 第一个方法解读完毕, 可以通过 javap 的结果对照查看:
- public bytecode.Test01();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: aload_0
- 5: iconst_0
- 6: putfield #2 // Field i:I
- 9: return
- LineNumberTable:
- line 7: 0
- line 8: 4
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 10 0 this Lbytecode/Test01;
属性(Attributes)
0x 00 01 00 12 00 00 00 02 00 13
0x 00 01: 表示包含一个属性
0x 00 12:attribute_name_index, 对应常量池中的值为: SourceFile
0x 00 00 00 02:attribute_length, 表示占据两个字节
0x 00 13: 对应常量池中的值为: Test01.java
来源: https://www.cnblogs.com/fx-blog/p/11982275.html