Java 语言以其“一次编写,处处运行”(Write Once, Run Anywhere)的特性而闻名。这一特性得益于 Java 虚拟机(JVM)的存在,它能够加载并执行与平台无关的字节码(Byte Code)。本文将通过 javap
命令来深入探讨 Java 字节码的结构和内容,帮助读者更好地理解 Java 程序的运行机制。
1 Java 字节码与 JVM
Java 源代码在编译后会生成 .class
文件,这些文件包含了 Java 字节码。字节码是一种中间代码,它不是直接的机器码,而是由 JVM 解释执行的指令集。JVM 在不同的操作系统上都有相应的实现,因此 Java 程序可以在不同的平台上运行。
2 使用 javap 命令反编译字节码
javap
是 JDK 自带的一个命令行工具,主要用于反编译 .class
文件。通过 javap
命令,我们可以查看类的结构、方法、字段等信息,从而更好地理解字节码的组成。
javap
的基本用法:
用法:
javap <options> <classes>
其中, 可能的选项包括:
-help --help -?
输出此用法消息
-version
版本信息
-v -verbose
输出附加信息
-l
输出行号和本地变量表
-public
仅显示公共类和成员
-protected
显示受保护的/公共类和成员
-package
显示程序包/受保护的/公共类
和成员 (默认)
-p -private
显示所有类和成员
-c
对代码进行反汇编
-s
输出内部类型签名
-sysinfo
显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants
显示最终常量
-classpath <path>
指定查找用户类文件的位置
-cp <path>
指定查找用户类文件的位置
-bootclasspath <path>
覆盖引导类文件的位置
以下是一个简单的 Java 类:
public class Main {private int age = 18;public int getAge() {return age;}
}
编译生成 Main.class
文件后,可以使用 javap -v -p Main.class
命令来查看字节码的详细信息。
3 字节码文件的基本信息
通过 javap
命令输出的内容,我们可以看到字节码文件的基本信息,包括文件路径、修改日期、文件大小、SHA-256 校验和等。这些信息有助于确保文件的完整性和一致性。
Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.classLast modified 2021年4月15日; size 385 bytesSHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6Compiled from "Main.java"
public class com.itwanger.jvm.Mainminor version: 0major version: 55flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #3 // com/itwanger/jvm/Mainsuper_class: #4 // java/lang/Objectinterfaces: 0, fields: 1, methods: 2, attributes: 1
- minor version 和 major version:表示字节码的版本号,这里的主版本号为 55,对应 Java 11。
- flags:表示类的访问标志,如
ACC_PUBLIC
和ACC_SUPER
。 - this_class 和 super_class:分别指向当前类和父类的常量池索引。
4 常量池
常量池是字节码文件中的一个重要部分,它包含了类、字段、方法等的符号引用。常量池中的常量可以分为字面量和符号引用两类。理解常量池的结构和内容对于深入理解 Java 字节码至关重要。
4.1 常量池的基本概念
常量池可以理解为字节码文件中的资源仓库,主要存放两大类信息:
- 字面量(Literal):类似于 Java 中的常量概念,比如文本字符串、
final
常量等。 - 符号引用(Symbolic References):属于编译原理方面的概念,包括以下三种:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
Java 虚拟机在加载字节码文件时进行动态链接,字段和方法的符号引用只有在运行期转换后才能获得真正的内存地址。
4.2 常量池的结构
当前字节码文件中一共有 21 个常量,它们之间是有链接的。我们采用顺藤摸瓜的方式,从上依次往下看,那些被链接的常量我们就点到为止。
注:
#
号后面跟的是索引,索引从 1 开始,而不是从 0 开始。设计者考虑到“如果要表达不引用任何一个常量的含义时,可以将索引值设为 0 来表示”。=
号后面跟的是常量的类型,没有包含前缀CONSTANT_
和后缀_info
。
4.3 常量池的详细解析
常量池中的常量通过索引相互引用,形成一个复杂的网络结构。以下是部分常量池的内容:
Constant pool:#1 = Methodref #4.#18 // java/lang/Object."<init>":()V#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I#3 = Class #20 // com/itwanger/jvm/Main#4 = Class #21 // java/lang/Object#5 = Utf8 age#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lcom/itwanger/jvm/Main;#14 = Utf8 getAge#15 = Utf8 ()I#16 = Utf8 SourceFile#17 = Utf8 Main.java#18 = NameAndType #7:#8 // "<init>":()V#19 = NameAndType #5:#6 // age:I#20 = Utf8 com/itwanger/jvm/Main#21 = Utf8 java/lang/Object
第 1 个常量:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
- 类型:
Methodref
,表明是用来定义方法的。 - 指向:常量池中下标为 4 和 18 的常量。
第 4 个常量:
#4 = Class #21 // java/lang/Object
- 类型:
Class
,表明是用来定义类(或者接口)的。 - 指向:常量池中下标为 21 的常量。
第 21 个常量:
#21 = Utf8 java/lang/Object
- 类型:
Utf8
,UTF-8 编码的字符串。 - 值:
java/lang/Object
。
第 18 个常量:
#18 = NameAndType #7:#8 // "<init>":()V
- 类型:
NameAndType
,表明是字段或者方法的部分符号引用。 - 指向:常量池中下标为 7 和 8 的常量。
第 7 个常量:
#7 = Utf8 <init>
- 类型:
Utf8
,UTF-8 编码的字符串。 - 值:
<init>
,表明为构造方法。
第 8 个常量:
#8 = Utf8 ()V
- 类型:
Utf8
,UTF-8 编码的字符串。 - 值:
()V
,表明方法的返回值为void
。
组合起来的意思:Main 类使用的是默认的构造方法,来源于 Object
类。#4
指向 Class #21
(即 java/lang/Object
),#18
指向 NameAndType #7:#8
(即 "<init>":()V
)。
第 2 个常量:
#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I
- 类型:
Fieldref
,表明是用来定义字段的。 - 指向:常量池中下标为 3 和 19 的常量。
第 3 个常量:
#3 = Class #20 // com/itwanger/jvm/Main
- 类型:
Class
,表明是用来定义类(或者接口)的。 - 指向:常量池中下标为 20 的常量。
第 19 个常量:
#19 = NameAndType #5:#6 // age:I
- 类型:
NameAndType
,表明是字段或者方法的部分符号引用。 - 指向:常量池中下标为 5 和 6 的常量。
第 5 个常量:
#5 = Utf8 age
- 类型:
Utf8
,UTF-8 编码的字符串。 - 值:
age
,表明字段名为age
。
第 6 个常量:
#6 = Utf8 I
- 类型:
Utf8
,UTF-8 编码的字符串。 - 值:
I
,表明字段的类型为int
。
组合起来的意思:声明了一个类型为 int
的字段 age
。#3
指向 Class #20
(即 com/itwanger/jvm/Main
),#19
指向 NameAndType #5:#6
(即 age:I
)。
5 字段表集合
字段表用来描述接口或者类中声明的变量,包括类变量和成员变量,但不包含声明在方法中的局部变量。字段表中的信息包括字段的修饰符、类型和名称等。
5.1 字段的修饰符
字段的修饰符一般有以下几种:
- 访问权限修饰符:如
public
、private
、protected
。 - 静态变量修饰符:如
static
。 - final 修饰符:如
final
。 - 并发可见性修饰符:如
volatile
。 - 序列化修饰符:如
transient
。
5.2 字段的类型和名称
字段的类型可以是基本数据类型(如 int
、float
)、数组和对象。名称则是字段的标识符。
5.3 字段表示例
在 Main.class
字节码文件中,字段表的信息如下所示:
private int age;descriptor: Iflags: (0x0002) ACC_PRIVATE
- 访问权限修饰符:
private
,表明字段的访问权限为私有。 - 类型:
int
,类型描述符为I
。 - 名称:
age
,字段的名称。
字段的访问标志和类的访问标志非常类似,ACC_PRIVATE
表示字段是私有的。
6 方法表集合
方法表是 Java 字节码文件中的一个重要部分,它用来描述接口或者类中声明的方法,包括类方法、成员方法以及构造方法。方法的修饰符和字段略有不同,例如 volatile
和 transient
不能用来修饰方法,而方法的修饰符多了 synchronized
、native
、strictfp
和 abstract
。
6.1. 构造方法
构造方法是用于创建类实例的特殊方法。在 Main.class
字节码文件中,构造方法的信息如下所示:
public com.itwanger.jvm.Main();descriptor: ()Vflags: (0x0001) ACC_PUBLIC
- 声明:
public com.itwanger.jvm.Main();
这是Main
类的构造方法,用于创建Main
类的实例。它是公开的(public
)。 - 描述符:
descriptor: ()V
这表示构造方法没有参数(()
)并且没有返回值(V
,代表void
)。 - 访问标志:
flags: (0x0001) ACC_PUBLIC
,表示这个构造方法是公开的,可以从其他类中访问。
6.1.1 Code 属性
构造方法的 Code
属性包含了方法的具体实现细节:
Code:stack=2, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: bipush 187: putfield #2 // Field age:I10: returnLineNumberTable:line 6: 0line 7: 4LocalVariableTable:Start Length Slot Name Signature0 11 0 this Lcom/itwanger/jvm/Main;
- stack:最大操作数栈深度为 2。操作数栈是一个 LIFO(后进先出)栈,用于存放临时变量和中间结果。在构造方法中,
bipush
和aload_0
指令可能会同时需要栈空间,所以需要 2 个操作数栈深度。 - locals:局部变量所需要的存储空间为 1 个槽(slot)。局部变量表的容量以变量槽为最小单位,一个变量槽可以存放一个 32 位以内的数据类型。对于实例方法(如构造方法),局部变量表的第一个位置(索引 0)总是用于存储
this
引用。 - args_size:方法的参数个数为 1。这是因为有一个隐藏的
this
变量,只要不是静态方法,都会有一个当前类的对象this
存在。
6.1.2 字节码指令
aload_0
:加载局部变量表中的第一个变量(通常是this
)。invokespecial #1
:调用父类的构造方法。bipush 18
:将整数 18 推送到操作数栈。putfield #2
:将操作数栈顶的值赋给字段age
。return
:返回。
6.1.3 附加信息
- LineNumberTable:描述源码行号与字节码行号(字节码偏移量)之间的对应关系。例如,第 6 行对应的字节码行号为 0,第 7 行对应的字节码行号为 4。
- LocalVariableTable:描述帧栈中的局部变量与源码中定义的变量之间的关系。例如,
this
变量的作用域从指令偏移量 0 开始,持续整个方法的长度(长度为 11),并且被分配到局部变量表的第一个槽位(索引 0)。
6.2 成员方法 getAge()
成员方法 getAge()
的信息如下所示:
public int getAge();descriptor: ()Iflags: (0x0001) ACC_PUBLIC
- 声明:
public int getAge();
这是Main
类的成员方法,用于获取age
字段的值。它是公开的(public
)。 - 描述符:
descriptor: ()I
这表示方法没有参数(()
)并且返回类型为int
(I
)。 - 访问标志:
flags: (0x0001) ACC_PUBLIC
,表示这个方法是公开的,可以从其他类中访问。
6.2.1 Code 属性
getAge()
方法的 Code
属性包含了方法的具体实现细节:
Code:stack=1, locals=1, args_size=10: aload_01: getfield #2 // Field age:I4: ireturnLineNumberTable:line 9: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/itwanger/jvm/Main;
- stack:最大操作数栈深度为 1。
- locals:局部变量所需要的存储空间为 1 个槽(slot)。
- args_size:方法的参数个数为 1。这是因为有一个隐藏的
this
变量。
6.2.2 字节码指令
aload_0
:加载局部变量表中的第一个变量(通常是this
)。getfield #2
:获取字段age
的值,并将其推送到操作数栈顶。ireturn
:返回栈顶的整型值。
6.2.3 附加信息
- LineNumberTable:描述源码行号与字节码行号(字节码偏移量)之间的对应关系。例如,第 9 行对应的字节码行号为 0。
- LocalVariableTable:描述帧栈中的局部变量与源码中定义的变量之间的关系。例如,
this
变量的作用域从指令偏移量 0 开始,持续整个方法的长度(长度为 5),并且被分配到局部变量表的第一个槽位(索引 0)。
7 总结
通过 javap
命令,我们可以深入了解 Java 字节码的结构和内容。字节码文件包含了类的基本信息、常量池、字段表、方法表和字节码指令等。理解这些内容不仅有助于我们更好地调试和优化 Java 程序,还能加深对 Java 虚拟机工作原理的认识。
8 思维导图
9 参考链接
从javap的角度轻松看懂字节码