大家好,我是二哥呀,今天我拿了一把小刀,准备带大家解剖一下 Java 的类文件结构,也就是 .class 文件的内容结构,虽然它实际上是一串连续的二进制,由 0 和 1 组成,但我们仍然可以借助一些工具来看清楚它的真面目。
类文件结构=.class文件的结构=Class文件结构,这三个说法都是一个意思,.class是从文件后缀名的角度来说的,Class是从Java类的角度来说的,类文件结构就是 Class 的中文译名。
---这部分内容前面其实已经讲过,但为了保持这篇内容的完整性,就暂时保留了下来,已经掌握的同学可以略过 start----
计算机的世界里流传着这么一句话,“计算机科学领域的任何问题都可以通过增加一个中间层来解决”。对于 Java 来说,JVM 就是这么一个产物,“Write once, Run anywhere”之所以能实现,靠得就是 JVM,它能在不同的操作系统下运行同一份源代码编译后的 class 文件。
Java 是跨平台的,JVM 作为中间层,自然要针对不同的操作系统提供不同的实现。拿 JDK 11 来说,它的实现就有上图中提到的这么多种(目前最新版本已经是 JDK 21 了)。
通过不同操作系统的 JVM,我们的源代码就可以不用根据不同的操作系统编译成不同的二进制可执行文件了,跨平台的目标也就实现了。
那这个 class 文件到底是什么玩意呢?它是怎么被 JVM 识别的呢?
我们用 IDEA 编写一段简单的 Java 代码,文件名为 Hello.java。
package com.itwanger.jvm;
class Hello {
public static void main(String[] args) {
System.out.println("Hello!");
}
}
点击编译按钮后(也不用主动点,IDEA 会自动编译),IDEA 会帮我们生成一个名为 Hello.class 的文件,在 target/classes
的对应包目录下。直接双击打开后长下面这样子:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.itwanger.jvm;
class Hello {
Hello() {
}
public static void main(String[] args) {
System.out.println("Hello!");
}
}
看起来和源代码很像,只是多了一个空的构造方法,对吧?它是 class 文件被 IDEA 自带的反编译工具 Fernflower 反编译后的样子。那真实的 class 文件长什么样子呢?
可以在终端中通过 xxd Hello.class
命令来查看(前面我们已经讲过了,大家可以戳这个链接回看)。
这就是 class 文件的十六进制形式。
---这部分内容前面其实已经讲过,但为了保持这篇内容的完整性,就暂时保留了下来,已经掌握的同学可以略过 end----
类文件的内容通常可以分为下面这几部分,见下图。
01、魔数
回看 class 文件的十六进制形式截图。
第一行中有一串特殊的字符 cafebabe
,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 ClassFormatError
。
魔数 cafebabe
的中文意思显而易见,咖啡宝贝,再加上 Java 的图标本来就是一个热气腾腾的咖啡,可见 Java 与咖啡的渊源有多深。
02、版本号
紧跟着魔数后面的四个字节 0000 0037
分别表示副版本号和主版本号。也就是说,主版本号为 55(0x37 的十进制),也就是 Java 11 对应的版本号,副版本号为 0。
上一个 LTS 版本是 Java 8,对应的主版本号为 52,也就是说 Java 9 是 53,Java 10 是 54,只不过 Java 9 和 Java 10 都是过渡版本,下一个 LTS 版本是 Java 17,预计 2021 年 9 月份推出(从这里大家可以推断出这篇内容的初稿时间,哈哈哈)。
那现在是 2023年12月14日,Java 21 已经发布了。通过上面的方法,大家可以查看一下 Java 21 对应的版本号是多少,这个小作业就留给大家了,动动手,你会发现不一样的世界。
03、常量池
紧跟在版本号之后的是常量池,它包含了类、接口、字段和方法的符号引用,以及字符串字面量和数值常量。这些信息在编译时被创建,并在运行时被Java虚拟机(JVM)使用。
相当于一个资源仓库,主要存放量大类型常量:
- 字面量(Literals):字面量是不变的数据,主要包括数值(如整数、浮点数)和字符串字面量。例如,一个整数100或一个字符串"Hello World",在源代码中直接赋值,编译后存储在常量池中。
- 符号引用(Symbolic References):符号引用是对类、接口、字段、方法等的引用,它们不是由字面量值给出的,而是通过符号名称(如类名、方法名)和其他额外信息(如类型、签名)来表示。这些引用在类文件中以一种抽象的方式存在,它们在类加载时被虚拟机解析为具体的内存地址。
(这部分内容我们前面讲过,戳链接回顾一下)
好,接下来,我们通过实际的代码示例来看一下常量池到底是什么。
Java 定义了 boolean、byte、short、char 和 int 等基本数据类型,它们在常量池中都会被当做 int 来处理。我们来通过一段简单的 Java 代码了解下。
public class ConstantTest {
public final boolean bool = true;
public final char aChar = 'a';
public final byte b = 66;
public final short s = 67;
public final int i = 68;
}
布尔值 true 的十六进制是 0x01、字符 a 的十六进制是 0x61,字节 66 的十六进制是 0x42,短整型 67 的十六进制是 0x43,整型 68 的十六进制是 0x44。所以编译生成的整型常量在 class 文件中的位置如下图所示。
第一个字节 0x03 表示常量的类型为 CONSTANT_Integer_info,是 JVM 定义的 14 种常量类型之一,对应的还有 CONSTANT_Float_info、CONSTANT_Long_info、CONSTANT_Double_info 等,它们对应的标识分别是 0x04、0x05、0x06。
我用表格来简单表示下:
常量类型 | 标识符 | 描述符 |
---|---|---|
CONSTANT_Integer_info | 0x03 | int 类型字面量 |
CONSTANT_Float_info | 0x04 | float 类型字面量 |
CONSTANT_Long_info | 0x05 | long 类型字面量 |
CONSTANT_Double_info | 0x06 | double 类型字面量 |
对于 int 和 float 来说,它们占 4 个字节;对于 long 和 double 来说,它们占 8 个字节。来个 long 型的最大值观察下。
public class ConstantTest {
public final long ong = Long.MAX_VALUE;
}
来看一下它在 class 文件中的位置。05 开头,7f ff ff ff ff ff ff ff 结尾,果然占 8 个字节,以前知道 long 型会占 8 个字节,但没有直观的感受,现在有了(😁)。
接下来,我们再来看一段代码。
class Hello {
public final String s = "hello";
}
“hello”是一个字符串,它的十六进制为 68 65 6c 6c 6f
,我们来看一下它在 class 文件中的位置。
真诚点赞 诚不我欺
回复