文章目录
一、字节码
在 Java 中, JVM 可以理解的代码就叫做 字节码(即拓展名为 .class
的文件)。对于 Java 代码,计算机是不能直接运行的,必须要先运行 java 虚拟机,再由 java 虚拟机运行编译后的 java 代码,这个编译后的 java 代码,就是 java 字节码。
为什么 jvm 不能直接运行 java 代码呢?
这是因为在 cpu 层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java 是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以 java 代码必须要先编译成字节码文件,jvm 才能正确识别代码转换后的指令并将其运行。
它不面向任何特定的处理器,只面向虚拟机。Java 代码间接翻译成字节码,存储字节码的文件再交由运行于不同平台的 JVM 虚拟机去读取执行,从而实现一次编写,到处运行的目的。
JVM 也不再只支持 java,由此衍生出了许多基于 JVM 的编程语言,如 Groovy,Scala,Koltin 等等。
二、Java 字节码文件
class 文件本质上是一个以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在 class 文件中,中间无任何分隔符,这使得整个 Class 文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用 8 位字节以上空间的数据项时,会按照高位在前的方式分割成若干个 8 位字节进行存储。
Java 虚拟机规范规定 Class 文件格式采用一种类似与 C 语言结构体的微结构体来存储数据,这种伪结构体中只有两种数据类型:无符号数和表。
无符号数 属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
表 是由多个无符号数或其它表作为数据表构成的符合数据类型,为了与无符号数以及其它结构进行区分,所有表的命名都习惯性的以"_info"结尾。例如,filed_info、method_info、attribute_info 等。
1. Class 文件的结构
整个 Class 文件就是一张表,它由下表中所示的数据项构成。
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔数 |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量池计数器 |
cp_info | constant_pool | contant_pool_count-1 | 常量池 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interfaces_count | 1 | 接口计数器 |
u2 | interfaces | interfaces_count | 接口索引集合 |
u2 | fields_count | 1 | 字段表计数器 |
field_info | fields | fields_count | 字段表集合 |
u2 | methods_count | 1 | 方法表计数器 |
method_info | methods | methods_count | 方法表集合 |
u2 | attributes_count | 1 | 属性表计数器 |
attribute_info | attributes | attributes_count | 属性表集合 |
Class 文件中存储的字节严格按照上表中的顺序紧凑的排列在一起。哪个字节代表什么含义,长度是多少,先后顺序如何都是被严格限制的,不允许有任何改变。其中u1、u2、u4、u8代表几个字节的无符号数,在反编译出来的16进制文件中,两个数字代表一个字节,也就是u1。
2. Class 文件结构属性
2.1 魔数与 Class 文件版本
u4 magic; // Class 文件的表标志
u2 minor_version; // Class 的小版本号
u2 major_version; // class 的大版本号
每个 Class 文件的头 4 个字节称为 魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。之所以使用魔数而不是文件后缀名来进行识别主要是基于安全的考虑,因为文件后缀名是可以随意更改的。 Java 规范规定魔数为固定值:0xCAFEBABE。如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它。
紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 两个字节是次版本号(Mainor Version),第 7 和第 8 个字节是主版本号(Major Version)。高版本的 JDK 能够向下兼容低版本的 Class 文件,虚拟机会拒绝执行超过其版本号的 Class 文件。
JDK 版本是从 45 开始的,JDK 1.0-1.1 使用了 45.0-45.3,所以 JDK1.8 对应的主版本号为 52
由于次版本号在 JDK 12 之前都没有使用过,全为 0
2.2 常量池
u2 constant_pool_count; // 常量池的数量
cp_info constant_pool[constant_pool_count - 1]; // 常量池
主版本号之后是常量池的常量数量,常量池的数量是「constant_pool_count - 1」 。因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型的数据来表示常量池的容量 「constant_pool_count」,和计算机科学中计数的方法不一样,这个容量是从 1 开始而不是从 0 开始计数。之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项」的含义,这种情况可以把索引值置为 0 来表示。
Class 文件结构中只有常量池的容量计数是从 1 开始的,其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。
之后就是常量池的实际内容**「constant_pool」**,常量池可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其它项关联最多的数据类型,也是占用 Class 文件空间最大的数据项之一,同时它还是 Class 文件中第一个出现的表类型数据项。
常量池中主要存放两大类常量: 字面量 和 符号引用。
- 字面量 比较接近 Java 语言层面的常量概念,如字符串、声明为 final 的常量值等。
- 符号引用 属于编译原理方面的概念,包含了以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
经过 javac 编译后的 Class 文件不会保存方法、字段最终在内存中的布局信息,而是保存其具体地址的符号引用。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析到具体的内存地址中。
关于字节码的类型对应如下:
标识字符 | 含义 |
---|---|
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型,以分号结尾,如 Ljava/lang/Object; |
[ | 数组类型,以分号结尾,如 [java/lang/Object; |
2.2.1 常量池表项目类型
在 JDK 1.8 中有 14 种常量池项目类型,每一个项目都有特定的表结构,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
常量池 tag 类型表:
常量类型 | 标志(tag) | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
2.2.2 常量池表结构
常量 | 描述 | 项目 | 类型 | 项目描述 |
CONSTANT_Utf8_info | UTF-8 编码的字符串 | tag | u1 | 值为 1 |
length | u2 | UTF-8 编码的字符串占用的字节数 | ||
bytes[length] | u1 | 长度为 length 的 UTF-8 编码的字符串 | ||
CONSTANT_Integer_info | 整型字面量 | tag | u1 | 值为 3 |
bytes | u4 | 按照高位在前存储的 int 值 | ||
CONSTANT_Float_info | 浮点型字面量 | tag | u1 | 值为 4 |
bytes | u4 | 按照高位在前存储的 float 值 | ||
CONSTANT_Long_info | 长整型字面量 | tag | u1 | 值为 5 |
bytes | u8 | 按照高位在前存储的 long 值 | ||
CONSTANT_Double_info | 双精度浮点型字面量 | tag | u1 | 值为 6 |
bytes | u8 | 按照高位在前存储的 double 值 | ||
CONSTANT_Class_info | 类或接口的符号引用 | tag | u1 | 值为 7 |
name_index | u2 | 指向全限定名常量项的索引 | ||
CONSTANT_String_info | 字符串类型字面量 | tag | u1 | 值为 8 |
string_index | u2 | 指向字符串字面量的索引 | ||
CONSTANT_Fieldref_info | 字段的符号引用 | tag | u1 | 值为 9 |
class_index | u2 | 指向声明字段的类或接口描述符 CONSTANT_Class_info 的索引项 | ||
name_and_type_index | u2 | 指向字段描述符 CONSTANT_NameAndType 的索引项 | ||
CONSTANT_Methodref_info | 类中方法的符号引用 | tag | u1 | 值为 10 |
class_index | u2 | 指向声明方法的类描述符 CONSTANT_Class_info 的索引项 | ||
name_and_type_index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType 的索引项 | ||
CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 | tag | u1 | 值为 11 |
class_index | u2 | 指向声明方法的接口描述符 CONSTANT_Class_info 的索引项 | ||
name_and_type_index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType 的索引项 | ||
CONSTANT_NameAndType_info | 字段或方法的部分符号引用 | tag | u1 | 值为 12 |
name_index | u2 | 指向该字段或方法名称常量项的索引 | ||
descriptor_index | u2 | 指向该字段或方法描述符常量的索引 | ||
CONSTANT_MethodHandle_info | 表示方法句柄 | tag | u1 | 值为 15 |
reference_kind | u1 | 值必须在 1~9 范围,它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 | ||
reference_index | u2 | 值必须是对常量池的有效索引 | ||
CONSTANT_MethodType_info | 标识方法类型 | tag | u1 | 值为 16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_utf8_info 结构,表示方法的描述符 | ||
CONSTANT_InvokeDynamic_info | 表示一个动态方法调用点 | tag | u1 | 值为 18 |
boostrap_method_attr_index | u2 | 值必须是对当前 Class 文件中引导方法表的 boostarp_methods[] 数组的有效索引 | ||
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_NameAndType_info 结构,表示方法名和方法描述符 |
javap 是 jdk 自带的反解析工具。它的作用就是根据 class 字节码文件,反解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。(javap -v class类名
Or javap -v class类名 -> temp.txt
(将结果输出到 temp.txt 文件 ))。
2.3 访问标志
u2 access_flag; // Class 的访问标志
在常量池之后的两个字节代表访问标志(access_flag),这个标志用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口;是否定义为 public
类型;是否定义为 abstract
类型;如果是类的话,是否被声明为 final 等。具体的标志位以及标志的含义见下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语言,invokespecial 指令的语意在 JDK 1.0.2 中发生过改变,微聊区别这条指令使用哪种语意,JDK 1.0.2 编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码维护 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中的 9 个,没有使用到的标志位要求一律为 0。
2.4 类索引、父类索引与接口索引集合
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 接口数量
u2 interfaces[interfaces_count]; // 一个类可以实现多个接口
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据集合。
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
- 接口索引集合用于描述这个类实现了哪些接口
类索引、父类索引、接口索引都排在访问标志之后。由于所有的类都是 java.lang.Object 类的子类,因此除了 Object 类之外所有类的的父类索引都不为 0。
类索引和父类索引各自指向 CONSTANT_Class_info 的类描述常量,通过 CONSTANT_Class_info 的类型常量中的索引可以找到 CONSTANT_utf8_info 类型的常量中的全限定名字符串,从而获取到该类的全限定名。
2.5 字段表集合
u2 fields_count; // 字段数量
field_info fields[field_count]; // 一个类可以有多个字段
字段表集合(field_info)用于描述接口或者类中声明的变量。字段(field)包括 类变量
和 实例变量
,但不包括方法内部声明的局部变量。
字段表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段修饰符放在 access_flags 中可设置的标识符如下,它与类中的访问标志(access_flag)非常相似,都是一个 u2 的数据类型。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字符是否为 public |
ACC_PRIVATE | 0x0002 | 字段是否为 private |
ACC_PROTECTED | 0x0004 | 字段是否为 protected |
ACC_STATIC | 0x0008 | 字段是否为 static |
ACC_FINAL | 0x0010 | 字段是否为 final |
ACC_VOLATILE | 0x0040 | 字段是否为 volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为 transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动生成 |
ACC_ENUM | 0x4000 | 字段是否为 enum |
2.6 方法表集合
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 一个类可以有多个方法
Class 文件中对方法的描述和对字段的描述是完全一致的,方法表中的结构表和字段表的结构一样。
因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT。与之相对的,synchronizes、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的访问标志中增加了 ACC_SYSNCHRONIZED、ACC_NATIVE、ACC_STICTFP 和 ACC_ABSTRACT 标志。
对于方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表中一个名为 「Code」的属性里面。
方法表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法表的 access_flags
取值:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字符是否为 public |
ACC_PRIVATE | 0x0002 | 字段是否为 private |
ACC_PROTECTED | 0x0004 | 字段是否为 protected |
ACC_STATIC | 0x0008 | 字段是否为 static |
ACC_FINAL | 0x0010 | 字段是否为 final |
ACC_SYNCHRONIED | 0x0020 | 方法是否为 synchronized |
ACC_BRIDGE | 0x0040 | 方法是不是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接收不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为 native |
ACC_ABSTRACT | 0x0400 | 方法是否为 abstract |
ACC_STRICT | 0x0800 | 方法是否为 strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动生成 |
2.7 属性表集合
u2 attributes_count; // 此类的属性表中的属性数
attibute_info attributes[attributes_count]; // 属性表集合
在 Class 文件、字段表、方法表中都可以携带自己的属性表(attribute_info)集合,用于描述某些场景专有的信息。
属性表集合不像 Class 文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机在运行时会忽略掉它不认识的属性。在《Java虚拟机规范(JavaSE7)》版本中,预定义属性有21项,此处只展示示例代码所涉及属性表结构。
Code:
Java 源文件方法体中的代码经过编译后,最终存储在Code属性内,它的结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | arrribute_name_index | 1 | 指向常量池中某个常量的索引,取值固定为 Code |
u4 | attribute_length | 1 | 属性值的长度 |
u2 | max_stack | 1 | 操作数栈的最大深度(jvm 运行时会根据这个值来分配栈帧中的操作数栈深度) |
u2 | max_locals | 1 | 局部变量表所需要的存储空间,单位为 slot |
u4 | code_length | 1 | 字节码指令长度 |
u1 | code | code_length | 具体的字节码指令(根据 jvm 规范,每个字节码指令占用一个字节,jvm 可以自动识别该指令是否需要接收参数) |
u2 | exception_table_length | 1 | 异常表个数 |
exception_info | exception_table | exceprion_table_length | 具体的异常表 |
u2 | attribute_count | 1 | 属性表个数 |
attribute_info | attributes | attribute_count | 属性表信息 |
其中 exception_info 结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | start_pc | 1 | 异常起始行 |
u2 | end_pc | 1 | 异常结束行 |
u2 | handler_pc | 1 | 出现异常,跳转行 |
u2 | catch_type | 1 | 异常类型(当 catch_type 为 0 时,代表任何异常情况都需要转向到 handler_pc 处进行处理) |
LineNumberTable :
LineNumberTable是Code属性中的一个子属性,用来描述java源文件行号与字节码文件偏移量之间的对应关系。当程序运行抛出异常时,异常堆栈中显示出错的行号就是根据这个对应关系来显示的,它的结构如下:
类型 | 名称 | 含义 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
其中 line_number_info 结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | start_pc | 1 | 字节码偏移量 |
u2 | line_number | 1 | java 源文件行号 |
SourceFile:
SourceFile属性用于记录生成这个Class文件的源码文件名称,它的结构如下:
名称 | 类型 | 数量 |
---|---|---|
attribute_name_index | u2 | 1 |
attribute_length | u4 | 1 |
sourcefile_index | u2 | 1 |
三、示例
通过 Test.java 进行 Class 文件解读学习参考。
代码:
public class Test {
private int m;
public int inc(){
return m + 1;
}
}
通过以下命令,可以在当前所在路径下生成一个 Test.class 文件。
javac Test.java
二进制信息:
- 使用 IDEA 插件 BinEd
- 使用 Sublime 查看
- vim 查看,输入
:%!xxd
即可查看二进制信息
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 5465 7374 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 5465 7374
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0500
0100 0d00 0000 0200 0e
cafe babe:Class 文件魔数。
0000:次版本号:JDK12 之前没有次版本号。
0034:主版本号 JDK8
0013:常量池数为 19
0a:第一个常量标志位 10,代表 Methodref(类中方法的符号引用)
0004:指向第 4 个常量(java/lang/Object),代表 class_index
000f:指向第 15 个常量(<init>:()V),代表 name_and_type_index
09:第二个常量标志位 9,代表字段的符号引用
0003:指向第 3 个常量(Test),代表 class_index
0010:指向第 16 个常量(m:I),代表 name_and_type_index
07:第三个常量标志位 7
0011:指向第 17 个常量(Test),代表 class_index
07:第四个常量标志位 7
0012:指向第 18 个常量(java/lang/Object),代表 class_index
01:第五个常量标志位 1
0001:字符串占用 1 个字节
6d:解码为:m
01:第六个常量标志位 1
0001:字符串占用 1 个字节
49:解码为:I,代表 int 类型
01:第七个常量标志位 1
0006:字符串占用 6 个字节
3c696e69743e:解码为:<init>
01:第八个常量标志位 1
0003:字符串占用 3 个字节
282956:解码为:()V,代表无参数,无返回值
01:第九个常量标志位 1
0004:字符串占用 4 个字节
436f6465:解码为:Code
01:第十个常量标志位 1
000f:字符串占用 15 个字节
4c696e654e756d6265725461626c65:解码为:LineNumberTable
01:第十一个常量标志位 1
0003:字符串占用 3 个字节
696e63:解码为:inc
01:第十二个常量标志位 1
0003:字符串占用 3 个字节
282949:解码为:()I,代表无参数,返回值类型为 int
01:第十三个常量标志位 1
000a:字符串占用 10 个字节
536f7572636546696c65:解码为:SourceFile
01:第十四个常量标志位 1
0009:字符串占用 9 个字节
546573742e6a617661:解码为:Test.java
0c:第十五个常量标志位 12,代表 NameAndType(字段或方法的部分符号引用)
0007:指向第 7 个常量(<init>),代表 name_index
0008:指向第 8 个常量(()V),代表 descriptor_index
0c:第十六个常量标志位 12,代表 NameAndType(字段或方法的部分符号引用)
0005:指向第 5 个常量(m),代表 name_index
0006:指向第 6 个常量(I),代表 descriptor_index
01:第十七个常量标志位 1
0004:字符串占用 4 个字节
54657374:解码为:Test
01:第十八个常量标志位 1
0010:字符串占用 16 个字节
6a6176612f6c616e672f4f626a656374:解码为:java/lang/Object
0021:该类为 Java 类,被 public 修饰,没有被声明为 final 或 abstract
0003:当前类索引,指向常量池第 3 个常量(Test)
0004:父类索引,指向常量池第 4 个常量(java/lang/Object)
0000:实现 0 个接口
0001:字段数量为 1
0002:第一个字段被 private 修饰
0005:指向常量池第 5 个常量(m),代表 name_index
0006:指向常量池第 6 个常量(I),代表 descriptor_index
0000:属性表计数器为 0,没有额外描述信息
// 第一个字段: 被 private 修饰的常量 m,类型为 int
0002:方法数量为 2
0001:第一个方法被 public 修饰
0007:指向常量池第 7 个常量(<init>),代表 name_index
0008:指向常量池第 8 个常量(()V),代表 descriptor_index
// 第一个方法: 被 public 修饰的方法 <init>,无参数,无返回值。
0001:属性表计数器为 1,表示此方法的属性集合有 1 项属性
0009:第一个属性表的名称索引指向常量池第 9 个常量(Code)
0000001d:属性值长度为 29
0001:操作数栈深度为 1
0001:局部变量表所需变量槽数为 1
00000005:字节码指令长度为 5
2a:对应指令为 aload_0:将变量槽第一个为 reference 类型的本地变量压入操作数栈栈顶
b7:对应指令为 invokespecial,以栈顶的 reference 类型数据指向的对象作为方法的接收者,调用此对象的实例构造器方法,私有方法,父类方法(接收一个 u2 类型的参数说明具体调用哪个方法)
0001:指向常量池第一个常量,根据常量池得 <init>
b1:对应指令为 return,从当前方法返回void
0000:异常表长度为 0
0001:Code 属性表的子属性表个数为 1
000a:第一个属性表的名称索引指向常量池第 10 个常量(LineNumberTable)
00000006:属性长度为 6
0001:行号表长度为 1
0000:字节码偏移量为 0
0001:java 源文件行号为 1
0001:第二个方法被 public 修饰
000b:指向常量池第 11 个常量(inc),代表 name_index
000c:指向常量池第 12 个常量(()I),代表 descriptor_index
// 第二个方法:被 public 修饰的方法 inc,无参数,返回值类型为 int
0001:属性表计数器为 1,表示此方法的属性集合有 1 项属性
0009:第一个属性表的名称索引指向常量池第 9 个常量(Code)
0000001f:属性值长度为 31
0002:操作数栈深度为 2
0001:局部变量表所需变量槽数为 1
00000007:字节码指令长度为 7
2a:对应指令为 aload_0:将变量槽第一个为 reference 类型的本地变量压入操作数栈栈顶
b4:对应指令为 getfield:获取指定类的实例域,并将其值压入栈顶
00:对应指令为 nop:无操作
02:对应指令为 iconst_m1:将int型-1推送至栈顶
04:对应指令为 iconst_1:将int型1推送至栈顶
60:对应指令为 iadd:将栈顶两int型数值相加并将结果压入栈顶
ac:对应指令为 ireturn:从当前方法返回int
0000:异常表长度为 0
0001:属性表计数器为 1,表示此方法的属性集合有 1 项属性
000a:第一个属性表的名称索引指向常量池第 10 个常量(LineNumberTable)
00000006:属性长度为 6
0001:行号表长度为 1
0000:字节码偏移量为 0
0005:java 源文件行号为 5
0001:属性表计数器为 1,表示此类的属性表集合有 1 项属性
000d:第一个属性表的名称索引指向常量池第 13 个常量(SourceFile)
00000002:属性长度为 2
000e:指向常量池第 14 个常量(Test.java),代表 sourcefile_index
反编译字节码文件:
使用到 java 内置的一个反编译工具 javap 可以反编译字节码文件,用法:
javap <options> <classes>
其中
<options>
选项包括:-help --help -? 输出此用法消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示最终常量 -classpath <path> 指定查找用户类文件的位置 -cp <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置
输入命令 javap -v -p Test.class
查看输出内容:
Classfile /F:/Code/Study/JVM-Study/Test.class
Last modified 2024-10-3; size 265 bytes
MD5 checksum 7cab5513115a4982f1d0f04048c0f405
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // Test.m:I
#3 = Class #17 // Test
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 Test
#18 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 5: 0
}
SourceFile: "Test.java"