文章目录
- 0.前言
- 1. 引言
- 1.1 Java编译原理基础
- 1.2 Class文件在Java编译过程中的角色
- 2. Class文件的整体结构
- 2.1 Class 文件组成
- 3. Class文件的详细解析
- 3.1 魔数与版本号的作用和意义
- 3.2 常量池的结构和作用
- 3.3 访问标志的含义和可能的值
- 3.4 类索引、父类索引和接口索引集合的作用和构成
- 3.5 字段表和方法表
- 3.6 属性表的详细解析
- 4. Class文件在JVM中的角色
- 4.1 Class文件在JVM中的生命周期
- 4.2 JVM是如何加载和使用Class文件的
- 4.3 Class文件在动态链接、加载和运行机制中的作用
- 5. javap 命令详解
- 6. 十六进制编辑器 010 Editor 查看Class 文件
- 7. 使用jclasslib 工具查看字节码
- 8.参考文档
0.前言
在Java编译过程中,源代码首先会被编译器编译成为字节码文件,这些文件的后缀名为.class。然而,尽管.class文件在Java编程中占有重要地位,但是大多数Java开发者对其内部的结构和工作原理并不是非常了解。所以抽时间我整理一下,和大家一起学习进步。
本文旨在对Java的.class文件结构进行详细的剖析,让我们一起了解其内部的工作机制。希望大家在阅读本文后, 对Java虚拟机有更多的了解,并在日后的编程工作中更加得心应手。
1. 引言
在了解JVM Class文件结构剖析之前,我们需要对两个基础知识进行了解一下。java 编译原理
和Class文件在Java编译过程中的角色
1.1 Java编译原理基础
在Java编译过程中,对于每个阶段的示例如下:
- 词法分析
词法分析:这个阶段会将源代码分割成一系列的词元(Token)。每个词元都是源代码中的一个最小有意义的独立部分,例如关键字(如public, class等)、标识符、运算符等。
源代码:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
词法分析后,会得到一系列的词元(Token),如:public
, class
, HelloWorld
, {
, public
, static
, void
, main
, (
, String
, [
, ]
, args
, )
, {
, System
, .
, out
, .
, println
, (
, "Hello, World!"
, )
, ;
, }
, }
。
- 语法分析
语法分析:语法分析阶段会根据词元,构建出抽象语法树(Abstract Syntax Tree, AST)。AST是一种用于表示程序结构的树形结构,树的每个节点都代表程序代码中的一个构造(例如声明,表达式等)。
语法分析阶段生成的抽象语法树(AST)涵盖源代码的结构信息。以下是HelloWorld
类的简化版AST:
ClassDeclaration
├── Modifier: public
├── Name: HelloWorld
└── MethodDeclaration
├── Modifier: public
├── Modifier: static
├── ReturnType: void
├── Name: main
├── Parameter: String[] args
└── Block
└── MethodInvocation
├── Name: System.out.println
└── Argument: "Hello, World!"
- 语义分析
语义分析:这个阶段会检查源代码的语义是否正确。例如类型检查,确保赋值和操作符等的类型正确性。此外,语义分析还包括符号解析,将代码中的变量和类型名称解析为内部的符号引用。
举一例错误的赋值语句:
public class Test {
public static void main(String[] args) {
int a = "hello";
}
}
在语义分析阶段,编译器会检查变量类型是否正确。在这个例子中,尝试将字符串"hello"
赋值给整型变量a
,这是错误的,编译器会提示类型不匹配的错误。
- 代码生成
代码生成:在所有分析完成后,编译器会生成字节码,字节码是一种中间代码,可以在Java虚拟机(JVM)上执行。
在代码生成阶段,编译器会将以上的源代码转化为以下的Java字节码:
public class HelloWorld {
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
这是.class文件的内容,可以通过javap -c HelloWorld
命令查看。
1.2 Class文件在Java编译过程中的角色
-
字节码的载体:
Class文件包含了Java程序的字节码,也就是Java源代码编译之后的结果。Java编译器(如javac)会将Java源代码转化为字节码,字节码是一种中间语言,它比源代码更接近于机器语言。字节码的存在使得Java程序可以在任何安装了Java虚拟机的平台上运行,因为Java虚拟机能够解释和执行字节码。
-
实现跨平台特性:
字节码是一种平台无关的中间表示形式,它不依赖于特定的硬件和操作系统。这意味着只要一个设备安装了Java虚拟机,那么这个设备就能够执行Java程序,无论这个设备使用的是什么操作系统。这就是Java的"一次编写,到处运行"的理念。
-
动态加载和链接:
Java虚拟机在运行时会动态地加载Class文件。也就是说,Java虚拟机并不会一次性加载所有的Class文件,而是在程序运行过程中,当需要使用到某个类时,才会将这个类的Class文件加载进内存。这种动态加载的机制提高了内存的使用效率。
除了加载,Java虚拟机还会进行链接。链接是指将Class文件中的符号引用替换为直接引用的过程。符号引用是一种抽象的引用,它可以是任何形式的字符集,用来描述被引用的信息。直接引用则是指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄。
-
存储元数据:
Class文件中包含了Java类或接口的元数据信息。这些信息包括类名,方法名,字段名以及它们的访问修饰符等。这些元数据在运行时可以被Java虚拟机用于反射、泛型等操作。
反射的操作包括获取Class实例,获取类的构造方法,字段和方法,创建类的实例,调用方法,访问字段等。
泛型则是Java语言提供的一种代码抽象和复用机制。Java的泛型是在编译器这个层次来实现的,也就是说,Java的泛型仅仅是给编译器Java源码用的,确保数据的类型安全。而字节码文件中,是不包含泛型中的类型信息的。在编译器编译Java源码到字节码时,会将源码中的泛型擦除,而在需要的地方插入类型强制转换的代码来确保类型正确。
2. Class文件的整体结构
参考Oracle 官方文档 Chapter 4. The class File Format https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
2.1 Class 文件组成
部分名称 | 描述 |
---|---|
魔数 | 魔数是Class文件的头四个字节,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Java的魔数固定为0xCAFEBABE 。 |
版本号 | 版本号是Class文件的次要版本号和主要版本号,它们都占用两个字节。主要版本号确定了Class文件的版本,比如JDK 1.1的主要版本号为45,而JDK 1.2~1.8的主要版本号则分别为46~52。 |
常量池 | 常量池是Class结构的一部分,它用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区中。 |
访问标志 | 访问标志用于识别一些类或接口层次的访问信息,包括:是否为public,是否为abstract,是否为interface等。 |
类索引、父类索引 | 类索引和父类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。 |
接口索引集合 | 接口索引集合用于描述这个类实现了哪些接口,这些接口将被初始化到一个列表中。 |
字段表 | 字段表用于描述接口或类中声明的变量,变量包括类级别的类变量,还有就是实例变量,没有方法体中的局部变量。 |
方法表 | 方法表用于描述类或接口中声明的方法。 |
属性表 | 属性表用于描述某个类,方法和字段的附加信息。比如,可以设置一个字段是否被废弃(Deprecated);还可以设置方法的字节码等。 |
每个部分都承担着其自身的职责,真正让Java有"一次编写,到处运行"特性的就是字节码,字节码是存储在方法表的code属性里的。
以下是一个简单的Java程序示例,我们将逐步解析其生成的Class文件各部分:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
编译上述Java程序会得到一个HelloWorld.class文件,这个文件包含了字节码和其它相关信息。我们使用JDK自带的javap工具来解析这个Class文件:
javap -verbose HelloWorld
以下是解析结果的一部分:
Classfile /C:/HelloWorld.class
Last modified Mar 11, 2021; size 434 bytes
MD5 checksum 63bf0338975b5720bf154f48d8a5db14
public class HelloWorld
minor version: 0
major version: 59
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // HelloWorld
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // HelloWorld
#8 = Utf8 HelloWorld
#9 = Utf8 Code
#10 = Methodref #11.#12 // java/lang/System.out:Ljava/io/PrintStream;
#11 = Class #13 // java/lang/System
#12 = NameAndType #14:#15 // out:Ljava/io/PrintStream;
#13 = Utf8 java/lang/System
#14 = Utf8 out
#15 = Utf8 Ljava/io/PrintStream;
#16 = String #17 // Hello, World!
#17 = Utf8 Hello, World!
#18 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#19 = Class #21 // java/io/PrintStream
#20 = NameAndType #22:#23 // println:(Ljava/lang/String;)V
#21 = Utf8 java/io/PrintStream
#22 = Utf8 println
#23 = Utf8 (Ljava/lang/String;)V
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 SourceFile
#27 = Utf8 HelloWorld.java
#28 = Utf8 LineNumberTable
#29 = Utf8 LocalVariableTable
#30 = Utf8 this
#31 = Utf8 LHelloWorld;
#32 = Utf8 args
#33 = Utf8 [Ljava/lang/String;
{
public HelloWorld();
descriptor: ()V
flags: (0x0001) 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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang.String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #16 // String Hello, World!
5: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
从中我们可以看到:
-
魔数、版本号:javap工具并未直接显示,但我们知道每个Class文件的开头四个字节就是魔数,而紧接着的4个字节代表版本号。
-
常量池:在"Constant pool"部分,包含了本程序中所使用到的各种常量和符号引用。
-
访问标志:在"flags"部分显示,
ACC_PUBLIC
表示这是一个public类,ACC_SUPER
是Java编译器为了支持某些编译优化而设定的。 -
类索引、父类索引:在"this_class"和"super_class"部分显示,分别表示这个类自身和其父类在常量池中的索引。
-
接口索引集合:本例没有实现任何接口,所以此部分为空。
-
字段表:本例没有定义任何字段,所以此部分为空。
-
方法表:在"Code"部分可以看到两个方法的字节码,一个是默认的构造函数
<init>
,一个是我们定义的main
方法。 -
属性表:在"SourceFile"部分显示,表示这个Class文件对应的源文件名。
3. Class文件的详细解析
3.1 魔数与版本号的作用和意义
魔数(Magic Number)是用来标识文件格式的一种约定,Java Class文件的魔数是0xCAFEBABE。当Java虚拟机加载Class文件时,首先会检查这个魔数,如果不是0xCAFEBABE,那么JVM会拒绝加载这个文件。
版本号(Version Number)包括主版本号和次版本号,主版本号变化通常代表JVM做了不兼容的修改,次版本号变化则表示JVM做了向后兼容的修改。JVM在加载Class文件时,也会检查版本号,如果文件的版本高于JVM的版本,那么JVM会拒绝加载这个文件。
3.2 常量池的结构和作用
常量池(Constant Pool)是Java Class文件结构的一个重要部分,它主要存储两类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量包括文本字符串、声明为final的常量值等;符号引用则包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
常量池的主要作用是为Java Class文件中的字面量和符号引用提供统一的存储空间,当Java虚拟机执行Class文件中的字节码时,会使用到常量池中的数据。
3.3 访问标志的含义和可能的值
访问标志(Access Flags)用于标识类或接口的访问权限和特性。如public(0x0001)、final(0x0010)、super(0x0020)、interface(0x0200)、abstract(0x0400)等。
3.4 类索引、父类索引和接口索引集合的作用和构成
类索引、父类索引和接口索引集合用于确定类的继承关系。类索引代表当前类,父类索引代表当前类的直接父类,接口索引集合代表当前类实现的所有接口。
3.5 字段表和方法表
字段表(Field Table)用于描述类或接口中声明的变量,包括字段的名称、描述符、访问标志等信息。方法表(Method Table)用于描述类或接口中声明的方法,包括方法的名称、描述符、访问标志、字节码等信息。
3.6 属性表的详细解析
属性表(Attribute Table)用于描述类、字段和方法的附加信息,例如源文件名(SourceFile)、字段的常量值(ConstantValue)、方法的字节码(Code)、已废弃信息(Deprecated)等。不同的属性有不同的内部结构,但都遵循“属性名索引-属性长度-属性信息”这样的基本格式。
4. Class文件在JVM中的角色
4.1 Class文件在JVM中的生命周期
Java虚拟机(JVM)对Class文件的处理可以分为以下几个阶段:
-
加载(Loading): 在这个阶段,JVM从本地磁盘、网络或者其他来源加载.class文件。加载完成之后,JVM将会创建一个表示这个类的
java.lang.Class
对象。 -
验证(Verification): 为了确保Class文件的正确性,JVM会对加载进来的字节码文件进行严格的验证,包括文件格式验证、元数据验证、字节码验证、符号引用验证等。如果验证失败,JVM将抛出
java.lang.VerifyError
异常。 -
准备(Preparation): 在这个阶段,JVM会为类变量(static变量)分配内存,并设置类变量的初始值。同时,JVM还会为类的方法表、接口表等分配内存。
-
解析(Resolution): 这个阶段主要执行符号引用到直接引用的转换。JVM将会解析类中的方法、字段、类、接口等的符号引用,将其替换为直接引用。
-
初始化(Initialization): 初始化阶段主要执行类构造器
<clinit>
方法,以及静态变量的赋值操作。这个阶段会确保类的初始化过程是线程安全的。 -
使用(Using): 类成功加载、验证、解析和初始化之后,就可以被程序使用了。这时,程序可以创建类的实例、调用方法、访问字段等。
-
卸载(Unloading): 当类不再被引用,并且在垃圾收集器运行后被标记为可回收时,JVM会卸载这个类。此时,JVM会释放与类相关的所有内存资源。
4.2 JVM是如何加载和使用Class文件的
JVM使用类加载器(ClassLoader)来加载和管理Class文件。类加载器负责将Class文件的字节码加载到内存中,并且执行验证、准备、解析等操作。在这个过程中,类加载器还会处理类之间的依赖关系,保证类的正确加载。
当程序需要使用一个类时,类加载器会首先查找是否已经加载过这个类。如果没有加载过,类加载器会根据类的全限定名(包名+类名)加载类的字节码,然后执行整个生命周期的操作。
一旦类被加载到JVM中,它就可以被程序使用。程序可以通过创建类的实例、调用方法、访问字段等方式使用这个类。
4.3 Class文件在动态链接、加载和运行机制中的作用
Class文件是Java程序在JVM中的载体。它包含了类的所有信息,包括类的属性、方法、常量池等。在Java程序执行的过程中,JVM通过动态链接、加载和运行机制来管理和使用Class文件。
-
动态链接:JVM在运行时将符号引用替换为直接引用的过程称为动态链接。Class文件中的符号引用包括方法引用、字段引用、类引用等。动态链接保证了Java代码可以在不同的环境下运行。
-
加载:JVM通过类加载器将Class文件加载到内存中。在这个过程中,JVM会执行验证、准备、解析等操作,确保类的正确性和可用性。
-
运行:加载完成的Class文件可以被程序使用。在运行期间,JVM会执行类的方法、访问字段、创建实例等操作。同时,JVM还会处理异常、垃圾回收等任务,保证程序的正常运行。
5. javap 命令详解
在上面的示例介绍中我们使用了一个命令
javap -verbose HelloWorld
那么我单独拉出来,给大家聊一下这个命令的作用。
javap
是Java虚拟机自带的反编译工具,用于从命令行解析class文件,打印出字节码、常量、构造方法、函数等等的信息。以下是javap
命令的一些常用选项:
-help
:查看帮助文档-version
:输出javap
的版本信息-v
或-verbose
:详细输出,输出额外的更为详细的信息,比如常量池信息、版本信息、字节码指令等-l
:输出行号和本地变量表-public
:只显示public的类和成员-protected
:只显示protected的类和成员-package
:只显示package的类和成员-private
:显示所有的类和成员-c
:对代码进行反汇编-s
:输出内部类型签名-sysinfo
:用于输出系统信息,包括主版本号、次版本号等-constants
:显示最终常量
也可以混合使用多个选项,如javap -private -c HelloWorld
可以显示HelloWorld中所有的类和成员,以及反汇编出的字节码。
假设我们有以下的一个简单的Java类HelloWorld.java
:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
首先,我们需要使用javac
命令编译这个Java文件,生成对应的Class文件:
javac HelloWorld.java
然后,我们就可以使用javap
命令查看生成的Class文件的内容了。我们先只查看Class文件的概述:
javap HelloWorld
输出如下:
Compiled from "HelloWorld.java"
public class HelloWorld {
public HelloWorld();
public static void main(java.lang.String[]);
}
这里我们可以看到,javap默认输出了Class文件的类名,以及其中的方法。
接下来,我们使用 -v
参数打印Class文件的详细内容:
javap -v HelloWorld
输出如下:
Classfile /path/to/HelloWorld.class
Last modified Apr 15, 2021; size 455 bytes
MD5 checksum 9d422a48a6a88f5c6ed5d6eb7cce5015
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#10 // java/lang/Object."<init>":()V
#2 = Fieldref #11.#12 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #13 // Hello, world!
#4 = Class #14 // java/lang/Object
#5 = Class #15 // HelloWorld
#6 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
...
...
这个输出包含了HelloWorld Class文件的很多详细信息,如版本号、访问标志、常量池等等。例如,我们可以在常量池中看到Hello, world!
这个字符串常量。
6. 十六进制编辑器 010 Editor 查看Class 文件
参考 https://bbs.kanxue.com/thread-257797.htm
参考 https://blog.csdn.net/freeking101/article/details/102908538
以下是您可以手动应用模板的方法,以防文件扩展名不是原始扩展名
这是模板结果的样子:
在十六进制/ASCII转储下方,显示了模板结果:一组嵌套字段,与.class文件的内部结构相匹配。例如,我在这里选择的第一个字段是u4 magic,它是一个.class文件的魔数标头:CAFEBABE。
7. 使用jclasslib 工具查看字节码
jclasslib 是一个功能丰富的字节码查看器,用于查看和分析 Java class 文件的结构。
- 下载和安装 jclasslib
官网 https://jclasslib.org/ 免费开源
-
打开 jclasslib
安装完成后,打开 jclasslib。会看到一个简洁的界面。
-
打开 class 文件
点击菜单栏的
File > Open...
选项,浏览的文件系统找到要查看的 .class 文件,选择它然后点击Open
。或者,也可以直接把 .class 文件拖拽到 jclasslib 的窗口中。
-
查看字节码
成功打开 .class 文件后,会在左侧看到一个树形结构,表示了 .class 文件的结构。可以点击树形结构的各个节点查看详细的信息。
右侧的窗口会根据选中的节点显示对应的详细信息。例如,如果选中了
constant_pool
节点,右侧就会显示常量池的所有内容。jclasslib 还提供了一个
Hex viewer
选项卡,可以在这里查看每个节点的原始字节码。
8.参考文档
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html