众所周知, Java 源文件会被编译为 .class 字节码文件, 然后交由 JVM 虚拟机执行. .class 文件相对于 .java 文件有着更强的表达性和结构性(更紧凑), 所以对 JVM 来说, .class 文件才是 “源码”.
今天, 我们就来深入浅出的看看 .class 文件的奥秘.
首先, 有个 Java 源码文件:
// Main.java
package com.walfud;
import jdk.jfr.Label;
import jdk.jfr.Name;
public class Main implements Runnable {
@Name("foo")
@Label("bar")
public static String s = "Hello, World!";
public int i = 2;
public static void main() {
System.out.println(s);
}
public void run() {
}
public void foo(int j) throws RuntimeException {
String s = "";
try {
s = "foo";
throw new RuntimeException();
} catch (Exception e) {
// nothing
s = "bar";
} finally {
s = s + ";";
}
System.out.println("done");
}
}
javac
是编译器, 将 java 源码文件编译为 .class 字节码文件.
# shell
javac Main.java
默认情况下 .class 文件只包含有限的调试信息, 比如代码行号和文件名. 可以使用
-g
参数要求编译器生成所有调试信息,-g:none
则不包含任何调试信息.--release
可以指定编译所使用的 JDK 版本, 例如 8, 11, 17 等.
.class 文件是个二进制文件, 有两种方法查看:
一种是直接打开二进制文件, 这里我们推荐 010 Editor 编辑器(跨平台, 支持各种模板, 可以解析 .class 文件).
过期后, Linux 下可以通过
rm -rf ~/.local/share/SweetScape/
重置.
另一种方法是通过 JDK 自带的 javap
命令, 他会自动解析 .class 文件, 将其中的各个字段格式化输出.
# shell
javap -v Main.class
-v
用于输出详细信息, 会包含 .class 文件中的所有信息.
为了探究 .class 文件的本质, 本文我们直接观察二进制文件方式来带大家解读其中的奥秘.
class 文件是一个结构化的二进制数据流, 整个文件以大端序进行组织. 文件的结构如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
.class 数据布局 (图片来自: https://blog.csdn.net/qq_43843037/article/details/107926711. 如有侵权请联系作者删除)
.class 文件有两种主要的数据类型: 一种是定长数据, 由 u1/u2/u4 分别代表无符号的 1/2/4 字节数据, 一般数据区的长度和各种位标记(bit flag) 都使用这种结构表示. 另一种是变长数据, 他们前 2 字节表示 length, 后面则是实际的数据. 例如 u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1];
这两个字段一起表示常量池的内容.