Java 文件都会被编译成 class 文件,那么 class 文件长什么样子呢?它记录了哪些信息呢?本文就来分析一下 class 文件的结构。
先写一段简单的 Java 程序
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
通过 javac 命令编译成 class 文件
javac Hello.java
执行这个命令后,就会在当前目录下生成 Hello.class 文件
通过十六进制编辑器 WinHex 打开这个 class 文件:
WinHex 下载地址:http://www.x-ways.net/winhex.zip
一、CAFEBABE
每个 Class 文件的头四个字节称为魔数,它的唯一作用是标识这是一个 Class 文件。为什么不采用扩展名来识别呢?这是因为扩展名可以随意改动。
Class 文件的魔数的值为 0xCAFEBABE,直译为咖啡宝贝,这个魔数似乎预示了 Java 标志性商标“咖啡”的出现。
二、版本号
紧接着四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号,第 7 和第 8 个字节是主版本号。JDK 1.1 之后,每个 JDK 大版本发布,主版本号加一。主版本号从 45 开始,例如:JDK 1.0~1.3 使用了 45.0~45.3 的版本号。高版本的 JDK 能向下兼容以前的 Class 文件,但每个 JDK 都不允许向上兼容,也就是不允许执行更高版本的 Class 文件。
可以看到,本例中,5~8 字节分别是 00 00 37 00,换算成 16 进制,也就是 55,对应 Java 11,通过 java -version 可以检验这个结论。
λ java --version
openjdk 11 2018-09-25
OpenJDK Runtime Environment 18.9 (build 11+28)
OpenJDK 64-Bit Server VM 18.9 (build 11+28, mixed mode)
三、常量池
版本号之后是常量池,可以通过二进制信息计算常量池的内容,也可以直接用 javap -verbose 命令看到常量池信息。
λ javap -verbose Hello
Classfile /D:/projects/Hello.class
Last modified 2022年1月4日; size 417 bytes
MD5 checksum 9bf221f2b01a4652155c0e0b9c8cdf81
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // Hello
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello, world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Hello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Hello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello, world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Hello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Hello();
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
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 #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
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Hello.java"
四、访问标志
常量池结束之后,紧接着的两个字节代表访问标志。包括:这个 class 是类还是接口,是否是 public,是否是 abstract,是否是 final 等等。
五、类索引、父类索引、接口索引集合
访问标志之后是类索引与父类索引,它们都是一个 u2 类型的数据,接口索引集合是一组 u2 类型数据的集合,这三项数据确定了该类的继承关系。
Java 中,只有 Object 类没有父类,它的父类索引为 0,其他的类都有且只有一个父类索引,因为 Java 是单继承的。
六、字段表集合
字段表用于描述接口或者类中声明的变量,但不包括在方法内部声明的局部变量。
字段的修饰符由标志位来表示,字段的名字、描述符通过引用常量池中的常量来表示。
描述符后还有一个属性表集合,用哦关于存储一些额外的信息。
比如:
final static int m = 123;
这个字段可能会存在一项名称为 ConstantValue 的属性,指向常量 123。
七、方法表集合
方法表和字段表类似,包括访问标志、名称索引、描述符索引、属性表集合。
八、属性表集合
1.Code 属性
Java 代码经过 javac 编译后,就会变成字节码指令存储在 Code 属性内。
Code 属性表记录了操作数栈最大深度(max_stack)、局部变量表所需的存储空间(max_locals)、编译后代码(code)、异常表(exception_table)等信息。
这里的异常表记录的是 try-catch 代码块中的异常信息
2.Exceptions 属性
这里的 Exceptions 属性和 Code 属性是平级的,不同于 Code 属性中的异常表,记录了方法声明中 throws 的异常。
3.LineNumberTable 属性
记录源码行号和字节码行号之间的对应关系。
4.LocalVariableTable 和 LocalVariableTypeTable
描述栈帧中局部变量表的变量与 Java 源码中定义的变量之间的关系。
5.SourceFile 及 SourceDebugExtension
SourceFile 用于记录生成这个 Class 文件的源码文件的名称。
SourceFile: "Hello.java"
SourceDebugExtension 用于存储额外的代码调试信息。
6.ConstantValue 属性
通知虚拟机自动为静态变量赋值。
7.InnerClasses 属性
记录内部类与宿主类之间的关联。
8.Deprecated 及 Synthetic 属性
标志着某个类、字段或者方法是否过时/是否是由编译器自动产生的
9.StackMapTable 属性
用于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
10.Signature 属性
记录泛型签名信息。
11.BootstrapMethods 属性
用于保存 invokedynamic 指令引用的引导方法限定符。
12.MethodParameters 属性
记录方法的各个形参名称和信息
13.模块化相关属性
14.运行时注解相关属性
参考文章
《深入理解 Java 虚拟机》(第三版)