类加载机制(1):类文件结构

30 阅读25分钟

一、编译的结果

在操作系统课程中,我们通常会学到:程序最终会被编译器翻译成由 01 组成的 二进制格式,并交给计算机执行。

不过,随着虚拟机技术的发展,以及大量运行在虚拟机之上的编程语言的出现,程序编译后的结果已经不再只有“本地机器码”这一种形式。

也就是说,源代码并不一定要直接编译成某个操作系统、某种 CPU 指令集专属的机器码。越来越多的语言会选择一种与具体操作系统、硬件平台无关的中间格式,作为程序编译后的存储格式。

Java 的 .class 文件就是这样一种平台无关的中间格式。

Java 源代码
    ↓ javac 编译
Class 字节码文件
    ↓ JVM 加载、验证、解释执行或 JIT 编译
机器执行

因此,.class 文件是 Java 实现“一次编译,到处运行”的重要基础。


二、Class 文件是什么?

任何一个 Class 文件,通常都对应着唯一的一个类或接口的定义信息。

不过需要注意的是,类或接口并不一定非要以磁盘文件的形式存在。它们也可以在运行期间被动态生成,然后直接交给类加载器加载。

例如,下面这些场景都可能动态生成 Class 字节码:

动态代理
CGLIB
ASM
Javassist
Lambda 表达式相关机制
运行时字节码增强

所以,Class 文件本质上不是“磁盘上的某个文件”这么简单,而是一种 JVM 规定好的 二进制数据格式

只要一段二进制数据满足 JVM 对 Class 文件格式的要求,它就可以被类加载器加载。


三、Class 文件的基本存储规则

Class 文件是一组以 8 位字节为基础单位的二进制流。

各个数据项会严格按照 JVM 规范规定的顺序,紧凑地排列在文件中。中间没有额外的分隔符,也没有空隙。

根据《Java 虚拟机规范》的定义,Class 文件格式采用一种类似于 C 语言结构体的伪结构来描述。

这种伪结构中只有两种基本数据类型:

无符号数
表

1. 无符号数

无符号数属于基本数据类型。

JVM 规范使用:

u1
u2
u4
u8

分别表示:

类型含义
u11 个字节的无符号数
u22 个字节的无符号数
u44 个字节的无符号数
u88 个字节的无符号数

无符号数可以用来描述:

数字
索引引用
数量值
按照 UTF-8 编码构成的字符串值

例如,constant_pool_count 是一个 u2 类型的数据,用来表示常量池容量计数值。


2. 表

表是由多个无符号数或者其他表作为数据项构成的复合数据类型。

为了便于区分,JVM 规范中的表通常会以 _info 结尾,例如:

cp_info
field_info
method_info
attribute_info

表通常用于描述具有层次关系的复合结构。

整个 Class 文件本身,也可以看作是一张大表。


3. 数量不确定的数据如何表示?

Class 文件中,经常会出现“同一类型的数据有多个,但数量不固定”的情况。

例如:

常量池中有多少个常量?
类实现了多少个接口?
类中有多少个字段?
类中有多少个方法?
方法中有多少个属性?

对于这种情况,Class 文件通常采用下面这种结构:

数量计数器 + 若干个连续的数据项

例如:

fields_count
fields[fields_count]

意思是:

先用 fields_count 表示字段数量
然后紧跟着 fields_count 个 field_info 结构

这种设计使得 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 文件
├── 魔数
├── 版本号
├── 常量池
├── 类的访问标志
├── 当前类
├── 父类
├── 接口表
├── 字段表
├── 方法表
└── 属性表

其中最重要的部分是:

常量池
字段表
方法表
属性表

尤其是 常量池Code 属性,它们是理解字节码执行机制的关键。


五、魔数和 Class 文件版本

1. 魔数

魔数,也叫 Magic Number,指的是文件格式中用于标识文件类型的固定字节序列。

它通常位于文件开头,用来帮助程序快速判断当前文件属于哪种格式。

每个 Class 文件的前 4 个字节固定为:

0xCAFEBABE

这 4 个字节就是 Class 文件的魔数。

它的作用是告诉 JVM:

当前文件是否是一个合法的 Class 文件。

如果 JVM 读取到一个文件,发现文件开头不是 0xCAFEBABE,就会认为它不是合法的 Class 文件。


2. Class 文件版本号

紧接着魔数之后的 4 个字节表示 Class 文件版本号。

第 5、6 个字节:minor_version,次版本号
第 7、8 个字节:major_version,主版本号

其中主版本号更常用,它决定了当前 Class 文件需要哪个版本的 JVM 才能运行。

例如:

Java 版本major_version
Java 852
Java 1155
Java 1761
Java 2165

如果使用低版本 JVM 去运行高版本编译出来的 Class 文件,就可能出现:

UnsupportedClassVersionError

例如,用 Java 8 的 JVM 去运行 Java 17 编译出来的 .class 文件,就可能出现该错误。


六、常量池

在主次版本号之后,紧接着就是常量池入口。

常量池可以理解为 Class文件中的 资源仓库

它是 Class 文件结构中与其他项目关联最多的数据区域,通常也是占用 Class 文件空间最大的数据项目之一。

同时,常量池也是 Class 文件中第一个出现的表类型数据项目。


1. 常量池容量计数器

由于常量池中的常量数量是不固定的,所以在常量池正式开始之前,会有一个 u2 类型的数据:

constant_pool_count

它表示常量池的容量计数值。

需要注意:

常量池索引从 1 开始,而不是从 0 开始。

因此,如果 constant_pool_count = 26,并不表示常量池中有 26 个有效常量项,而是表示有效索引范围通常是:

1 ~ 25

也就是说,实际常量池项数量通常是:

constant_pool_count - 1

这是 Class 文件结构中一个比较特殊的地方。


2. 常量池中存放什么?

常量池中的常量大致可以分成两大类:

字面量
符号引用

2.1 字面量

字面量比较接近 Java 语言层面的常量概念。

例如:

String s = "hello";
int x = 10;

其中:

"hello"
10

都可以理解为字面量。


2.2 符号引用

符号引用是常量池中更重要的部分。

Java 代码在 javac 编译时,并不像 C/C++ 那样在编译阶段完成完整的链接过程。

Java 的链接过程主要发生在类加载阶段,也就是 JVM 加载 Class 文件之后。

因此,在 Class 文件中不会直接保存某个方法、字段在内存中的最终地址,而是使用符号引用来描述它们。

符号引用可以包括:

类或接口的全限定名
字段名称和描述符
方法名称和描述符
方法句柄
方法类型
动态调用点

例如下面这段代码:

public class Hello {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

代码中会出现很多符号信息,例如:

Hello
main
java/lang/Object
java/lang/System
out
println
hello
([Ljava/lang/String;)V
(Ljava/lang/String;)V

这些信息通常都会进入常量池。


3.为什么字节码要依赖常量池?

对于下面这行代码:

System.out.println("hello");

编译成字节码后,可能类似:

getstatic     #2
ldc           #3
invokevirtual #4

这里的:

#2
#3
#4

就是常量池索引。

也就是说,字节码指令中并不会直接写完整的信息:

调用 java/io/PrintStream.println 方法

而是写成:

调用常量池中的第 4 项

这样做有两个好处:

第一,Class 文件结构更加紧凑。

第二,更适合 JVM 在运行时进行符号解析和动态链接。

可以简单理解为:

字节码指令负责“做什么”
常量池负责“具体是谁”

例如:

invokevirtual #4

表示:

执行一个虚方法调用,具体调用哪个方法,要去常量池第 4 项中查。

4.常量池中常见的常量类型

常量池中的每个元素都有自己的类型,常见类型如下:

常量池类型含义
CONSTANT_Utf8UTF-8 编码的字符串,比如类名、方法名、字段名
CONSTANT_Class类或接口的符号引用
CONSTANT_String字符串常量
CONSTANT_Fieldref字段引用
CONSTANT_Methodref方法引用
CONSTANT_InterfaceMethodref接口方法引用
CONSTANT_NameAndType名称和描述符
CONSTANT_Integerint 类型常量
CONSTANT_Longlong 类型常量
CONSTANT_Doubledouble 类型常量
CONSTANT_InvokeDynamic动态调用相关,常见于 Lambda 表达式

其中需要特别注意:

CONSTANT_Class、CONSTANT_Fieldref、CONSTANT_Methodref 等并不是直接保存目标对象本身,
而是保存对其他常量池项的引用。

也就是说,常量池内部也存在大量“索引指向索引”的结构。


七、类的基本信息

常量池之后,是类自身的一些描述信息,主要包括:

access_flags
this_class
super_class
interfaces
fields
methods
attributes

1.access_flags:类访问标志

access_flags 用来描述当前类或接口的访问属性。

例如:

public final class User {
}

这个类可能包含如下访问标志:

ACC_PUBLIC
ACC_FINAL
ACC_SUPER

常见访问标志如下:

标志含义
ACC_PUBLIC是否为 public 类型
ACC_FINAL是否为 final 类型
ACC_SUPERJVM 调用父类方法时使用的特殊语义
ACC_INTERFACE是否为接口
ACC_ABSTRACT是否为抽象类或接口
ACC_ANNOTATION是否为注解类型
ACC_ENUM是否为枚举类型

注意:

access_flags 是一个位标志集合。

也就是说,一个类可以同时拥有多个访问标志。


2.this_class:当前类

this_class 表示当前 Class 文件描述的是哪个类。

不过这里并不会直接保存类名字符串,而是保存一个指向常量池的索引。

例如:

this_class -> #7
#7 = Class              #8
#8 = Utf8               Hello

它的含义是:

当前类是 Hello。

这里的查找过程是:

this_class
    ↓
CONSTANT_Class
    ↓
CONSTANT_Utf8
    ↓
真正的类名

3. super_class:父类

super_class 表示当前类的父类。

例如:

public class Hello {
}

虽然代码中没有显式继承任何类,但 Java 中的普通类默认继承:

java.lang.Object

所以它的父类信息通常是:

super_class -> java/lang/Object

需要注意的是,在 Class 文件中,类名通常使用 / 分隔包路径,而不是使用 .

因此 Class 文件中会写成:

java/lang/Object

而不是:

java.lang.Object

4. interfaces:接口表

interfaces 用来描述当前类实现了哪些接口,或者当前接口继承了哪些父接口。

例如:

public class UserService implements Service, AutoCloseable {
}

那么接口表中会记录:

Service
AutoCloseable

不过和 this_classsuper_class 一样,接口表中直接保存的也不是接口名字符串,而是常量池索引。


5. fields:字段表

fields 用于描述当前类或接口中声明的字段。

例如:

public class User {
    private int age;
    private String name;
}

这个类中声明了两个字段:

age
name

因此 Class 文件中的字段表会有两个字段信息。

字段表中的每个字段都使用 field_info 结构表示。

field_info {
    u2 access_flags;                 // 字段访问标志
    u2 name_index;                   // 字段名索引
    u2 descriptor_index;             // 字段描述符索引
    u2 attributes_count;             // 字段属性数量
    attribute_info attributes[attributes_count];
}

可以把一个字段理解为由下面几部分组成:

字段访问权限
字段名
字段类型描述符
字段属性

5.1 字段描述符

字段描述符用于描述字段的数据类型。

常见类型描述符如下:

Java 类型描述符
intI
longJ
floatF
doubleD
booleanZ
charC
byteB
shortS
StringLjava/lang/String;
int[][I
String[][Ljava/lang/String;

例如:

private String name;

对应字段描述符是:

Ljava/lang/String;

再例如:

private int[] scores;

对应字段描述符是:

[I

需要注意:

void 的描述符是 V,但 void 只能用于方法返回值,不能用于字段类型。

6. methods:方法表

methods 用来描述当前类或接口中声明的方法。

例如:

public class Hello {
    public void sayHello() {
        System.out.println("hello");
    }
}

这个 Class 文件中至少会包含两个方法:

<init>
sayHello

为什么会有 <init>

因为即使你没有手动编写构造方法,Java 编译器也会自动生成一个默认构造方法。

例如:

public class Hello {
}

编译器会自动补充类似下面的构造逻辑:

public Hello() {
    super();
}

在 Class 文件中,实例构造方法的名字叫:

<init>

6.1 method_info 结构

方法表中的每个方法都使用 method_info 结构表示:

method_info {
    u2 access_flags;                 // 方法访问标志
    u2 name_index;                   // 方法名索引
    u2 descriptor_index;             // 方法描述符索引
    u2 attributes_count;             // 方法属性数量
    attribute_info attributes[attributes_count];
}

可以把一个方法理解为由下面几部分组成:

方法访问权限
方法名
方法描述符
方法属性

不过要注意:

method_info 本身并不直接等于方法的执行逻辑。

方法真正的字节码指令通常存放在它的 Code 属性中。


6.2 方法描述符

方法描述符用于描述方法的:

参数类型
返回值类型

格式如下:

(参数类型列表)返回值类型

例如:

public void test()

对应方法描述符是:

()V

含义是:

无参数,返回 void

再例如:

public int add(int a, int b)

对应方法描述符是:

(II)I

含义是:

两个 int 参数,返回 int

再例如:

public String hello(String name, int age)

对应方法描述符是:

(Ljava/lang/String;I)Ljava/lang/String;

含义是:

参数是 Stringint,返回 String

7. attributes:属性表

属性表是 Class 文件中非常重要的扩展机制。

很多信息并不是直接放在 Class 文件主结构中的,而是通过属性表来保存。

常见属性包括:

属性作用
Code方法的字节码指令
LineNumberTable字节码和源码行号的对应关系
LocalVariableTable局部变量信息
SourceFile源文件名
ConstantValue常量字段的值
Exceptions方法声明抛出的异常
InnerClasses内部类信息
Signature泛型签名
RuntimeVisibleAnnotations运行时可见注解
StackMapTable字节码验证使用的栈帧信息

属性表的设计非常灵活。

它使 JVM 可以在不破坏 Class 文件整体结构的前提下,不断扩展新的能力。

例如:

泛型
注解
Lambda
模块化
字节码验证
调试信息

很多能力都依赖属性表或常量池扩展来实现。


7.1 Code 属性

对于普通方法而言,真正的字节码指令通常存放在 Code 属性中。

例如:

public int add(int a, int b) {
    return a + b;
}

这个方法本身的 method_info 只记录:

方法名:add
方法描述符:(II)I
访问标志:public
方法属性:Code

真正的执行逻辑在 Code 属性里。

Code 属性的大致结构如下:

Code_attribute {
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    exception_table exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

其中几个核心字段是:

字段含义
max_stack操作数栈的最大深度
max_locals局部变量表所需的空间大小
code_length字节码指令长度
code真正的字节码指令
exception_table异常处理表
attributesCode 属性内部的其他属性

7.1.1 max_stack

max_stack 表示当前方法执行过程中,操作数栈可能达到的最大深度。

JVM 执行字节码时采用的是基于操作数栈的执行模型。

例如:

int c = a + b;

对应的字节码可能类似:

iload_1
iload_2
iadd
istore_3

执行过程可以理解为:

iload_1   将局部变量 a 压入操作数栈
iload_2   将局部变量 b 压入操作数栈
iadd      弹出两个 int,相加后把结果压回操作数栈
istore_3  将栈顶结果保存到局部变量 c

所以 JVM 需要提前知道当前方法执行时操作数栈最多需要多深,这就是 max_stack 的作用。


7.1.2 max_locals

max_locals 表示当前方法所需的局部变量表大小。

局部变量表中通常保存:

this 引用
方法参数
方法内部定义的局部变量

例如:

public int add(int a, int b) {
    int c = a + b;
    return c;
}

对于实例方法来说,局部变量表可能是:

slot内容
0this
1a
2b
3c

所以这个方法的 max_locals 至少需要 4 个 slot。

需要注意:

实例方法的局部变量表第 0 个 slot 通常是 this。
静态方法没有 thislongdouble 类型会占用两个 slot。

7.1.3 code

code 字段保存的就是真正的 JVM 字节码指令。

例如:

public int add(int a, int b) {
    return a + b;
}

对应字节码可能是:

0: iload_1
1: iload_2
2: iadd
3: ireturn

含义是:

iload_1   加载第 1 个局部变量,也就是 a
iload_2   加载第 2 个局部变量,也就是 b
iadd      执行 int 加法
ireturn   返回 int 类型结果

八、使用 javap 观察 Class 文件

可以使用 javap -v 查看一个 Class 文件的详细结构。

源文件如下:

public class TestClassFile {
    private int m = 10;
    private static final String MESSAGE = "hello class file";

    public TestClassFile() {
    }

    public int inc() {
        return m++;
    }

    public int add(int a, int b) {
        int c = a + b;
        return c;
    }

    public static void main(String[] args) {
        TestClassFile obj = new TestClassFile();
        int result = obj.add(1, 2);
        System.out.println(MESSAGE);
        System.out.println(result);
    }
}

其Class文件如下:

Classfile /mnt/data/classdemo/TestClassFile.class
  Last modified Jun 8, 2026; size 953 bytes
  SHA-256 checksum 62fc3507c5b639af60734c10c35d8fb982bad75751a1b6c812245daa1620f43a
  Compiled from "TestClassFile.java"
public class TestClassFile
  minor version: 0
  major version: 65
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // TestClassFile
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 4, 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 = Fieldref           #8.#9          // TestClassFile.m:I
   #8 = Class              #10            // TestClassFile
   #9 = NameAndType        #11:#12        // m:I
  #10 = Utf8               TestClassFile
  #11 = Utf8               m
  #12 = Utf8               I
  #13 = Methodref          #8.#3          // TestClassFile."<init>":()V
  #14 = Methodref          #8.#15         // TestClassFile.add:(II)I
  #15 = NameAndType        #16:#17        // add:(II)I
  #16 = Utf8               add
  #17 = Utf8               (II)I
  #18 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
  #19 = Class              #21            // java/lang/System
  #20 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = String             #25            // hello class file
  #25 = Utf8               hello class file
  #26 = Methodref          #27.#28        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #27 = Class              #29            // java/io/PrintStream
  #28 = NameAndType        #30:#31        // println:(Ljava/lang/String;)V
  #29 = Utf8               java/io/PrintStream
  #30 = Utf8               println
  #31 = Utf8               (Ljava/lang/String;)V
  #32 = Methodref          #27.#33        // java/io/PrintStream.println:(I)V
  #33 = NameAndType        #30:#34        // println:(I)V
  #34 = Utf8               (I)V
  #35 = Utf8               MESSAGE
  #36 = Utf8               Ljava/lang/String;
  #37 = Utf8               ConstantValue
  #38 = Utf8               Code
  #39 = Utf8               LineNumberTable
  #40 = Utf8               LocalVariableTable
  #41 = Utf8               this
  #42 = Utf8               LTestClassFile;
  #43 = Utf8               inc
  #44 = Utf8               ()I
  #45 = Utf8               a
  #46 = Utf8               b
  #47 = Utf8               c
  #48 = Utf8               main
  #49 = Utf8               ([Ljava/lang/String;)V
  #50 = Utf8               args
  #51 = Utf8               [Ljava/lang/String;
  #52 = Utf8               obj
  #53 = Utf8               result
  #54 = Utf8               SourceFile
  #55 = Utf8               TestClassFile.java
{
  private int m;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  private static final java.lang.String MESSAGE;
    descriptor: Ljava/lang/String;
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String hello class file
    
  public TestClassFile();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        10
         7: putfield      #7                  // Field m:I
        10: return
      LineNumberTable:
        line 5: 0
        line 2: 4
        line 6: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LTestClassFile;

  public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #7                  // Field m:I
         5: dup_x1
         6: iconst_1
         7: iadd
         8: putfield      #7                  // Field m:I
        11: ireturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   LTestClassFile;

  public int add(int, int);
    descriptor: (II)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_3
         5: ireturn
      LineNumberTable:
        line 13: 0
        line 14: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LTestClassFile;
            0       6     1     a   I
            0       6     2     b   I
            4       2     3     c   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #8                  // class TestClassFile
         3: dup
         4: invokespecial #13                 // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_1
        10: iconst_2
        11: invokevirtual #14                 // Method add:(II)I
        14: istore_2
        15: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: ldc           #24                 // String hello class file
        20: invokevirtual #26                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        23: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
        26: iload_2
        27: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V
        30: return
      LineNumberTable:
        line 18: 0
        line 19: 8
        line 20: 15
        line 21: 23
        line 22: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1   obj   LTestClassFile;
           15      16     2 result   I
}
SourceFile: "TestClassFile.java"

下面来分块对应解析:

1.javap额外信息

Classfile /mnt/data/classdemo/TestClassFile.class
  Last modified Jun 8, 2026; size 953 bytes
  SHA-256 checksum 62fc3507c5b639af60734c10c35d8fb982bad75751a1b6c812245daa1620f43a
  Compiled from "TestClassFile.java"

这部分是javap工具额外展示的文件信息。

2.魔数

因为是用javap查看的,因此不会直接展示魔数,如果使用十六进制工具查看那么就会看到0XCAFEBABE

3.版本号

minor version: 0
major version: 65

4.常量池

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   ...
   #55 = Utf8              TestClassFile.java

从上述可以看出,常量池就是整个Class文件的符号仓库

类名
父类名
字段名
字段描述符
方法名
方法描述符
字符串常量
字段引用
方法引用
属性名
局部变量名
源文件名

4.1 示例:父类构造方法引用

#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

#1本身并没有直接保存完整字符串,而是引用了其他常量池项,其他常量池项又引用其他其他常量池项。 把它串起来:

#1 Methodref
├── #2 Class
│   └── #4 Utf8 java/lang/Object
└── #3 NameAndType
    ├── #5 Utf8 <init>
    └── #6 Utf8 ()V

最终含义就是:

调用 java/lang/Object 的 <init>()V 方法

也就是 Java 代码中的:

super();

4.2 示例:字段引用m

#7 = Fieldref           #8.#9          // TestClassFile.m:I

它表示字段引用:

TestClassFile.m:I

也就是:

private int m;

继续展开:

#8 = Class              #10            // TestClassFile
#9 = NameAndType        #11:#12        // m:I
#10 = Utf8              TestClassFile
#11 = Utf8              m
#12 = Utf8              I

结构是:

#7 Fieldref
├── #8 Class
│   └── #10 Utf8 TestClassFile
└── #9 NameAndType
    ├── #11 Utf8 m
    └── #12 Utf8 I

其中:

m = 字段名
I = int 类型描述符

所以:

TestClassFile.m:I

意思是:

TestClassFile 类中的 int 类型字段 m

4.3 示例:add方法引用

#14 = Methodref          #8.#15         // TestClassFile.add:(II)I

它表示方法引用:

TestClassFile.add:(II)I

继续展开:

#8 = Class              #10            // TestClassFile
#15 = NameAndType        #16:#17        // add:(II)I
#16 = Utf8               add
#17 = Utf8               (II)I

结构是:

#14 Methodref
├── #8 Class
│   └── #10 Utf8 TestClassFile
└── #15 NameAndType
    ├── #16 Utf8 add
    └── #17 Utf8 (II)I

其中:

(II)I

表示:

参数是两个 int,返回值是 int

对应 Java 方法:

public int add(int a, int b)

4.4 示例:System.out.println

main 方法里有:

System.out.println(MESSAGE);
System.out.println(result);

所以常量池里有这些内容:

#18 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
#19 = Class              #21            // java/lang/System
#20 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
#21 = Utf8               java/lang/System
#22 = Utf8               out
#23 = Utf8               Ljava/io/PrintStream;

这表示字段:

System.out

也就是:

java/lang/System.out:Ljava/io/PrintStream;

然后是两个 println 方法:

#26 = Methodref          #27.#28        // java/io/PrintStream.println:(Ljava/lang/String;)V
#32 = Methodref          #27.#33        // java/io/PrintStream.println:(I)V

它们分别对应:

println(String)
println(int)

代码里打印了两个不同类型的值:

System.out.println(MESSAGE); // String
System.out.println(result);  // int

所以对应两个方法描述符:

(Ljava/lang/String;)V
(I)V

5.access_flags:类访问标志

flags: (0x0021) ACC_PUBLIC, ACC_SUPER

Class 文件结构中是:

u2 access_flags;

这里表示当前类的访问标志。

0x0021 = ACC_PUBLIC + ACC_SUPER

含义是:

ACC_PUBLIC:这个类是 public 的
ACC_SUPER:使用新的 invokespecial 语义调用父类方法

对应类定义:

public class TestClassFile {
}

因为类是 public,所以有:

ACC_PUBLIC

普通现代 Java 类通常也会带:

ACC_SUPER

6. this_class:当前类

this_class: #8                          // TestClassFile

Class 文件结构中是:

u2 this_class;

它表示当前 Class 文件描述的是哪个类。 这里:

this_class = #8

然后看常量池:

#8 = Class              #10            // TestClassFile
#10 = Utf8              TestClassFile

所以:

this_class: #8

最终表示:

当前类是 TestClassFile

注意,这里并不是直接保存字符串 TestClassFile,而是保存常量池索引。 查找链路是:

this_class
    ↓
#8 Class#10 Utf8 TestClassFile

7.super_class:父类

super_class: #2                         // java/lang/Object

Class 文件结构中是:

u2 super_class;

这里:

super_class = #2

看常量池:

#2 = Class              #4             // java/lang/Object
#4 = Utf8               java/lang/Object

所以当前类的父类是:

java/lang/Object

虽然源码里没有写:

extends Object

但是 Java 中所有普通类默认继承:

java.lang.Object

所以 Class 文件中会记录父类为:

java/lang/Object

8.interfaces_count和interfaces:接口表

interfaces: 0

Class 文件结构中是:

u2 interfaces_count;
u2 interfaces[interfaces_count];

这里表示:

interfaces_count = 0

也就是说:

public class TestClassFile {
}

这个类没有实现任何接口。如果类是这样:

public class TestClassFile implements Runnable {
}

那么这里就不会是 0,而是会记录 Runnable 对应的常量池索引。

9.fileds_count和fields:字段表

field: 2

表示这个类有 2 个字段。根据之前的源码,这两个字段是:

private int m = 10;
private static final String MESSAGE = "hello class file";

也就是:

字段 1:m
字段 2:MESSAGE

9.1 字段m

private int m = 10;

它的字段信息大致是:

private int m;
descriptor: I
flags: (0x0002) ACC_PRIVATE

其中:

m = 字段名
I = int 类型描述符

它在常量池中对应:

#11 = Utf8               m
#12 = Utf8               I
#7 = Fieldref            #8.#9          // TestClassFile.m:I

注意:

private int m = 10;

这个 10 并不是字段表里直接存的初始值。 它会被编译到构造方法 <init> 中:

4: aload_0
5: bipush        10
7: putfield      #7                  // Field m:I

也就是说,对实例字段 m 的初始化是在构造方法中完成的。

9.2 字段MESSAGE

private static final String MESSAGE = "hello class file";

它会对应:

private static final java.lang.String MESSAGE;
    descriptor: Ljava/lang/String;
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String hello class file

它在常量池里能看到相关信息:

#24 = String             #25            // hello class file
#25 = Utf8               hello class file
#35 = Utf8               MESSAGE
#36 = Utf8               Ljava/lang/String;
#37 = Utf8               ConstantValue

这里要注意:

private static final String MESSAGE = "hello class file";

因为它是编译期常量,所以值会记录在字段的 ConstantValue 属性中。

并且在 main 方法中使用它时,字节码是:

18: ldc           #24                 // String hello class file

也就是说,main 方法并没有通过 getstatic 去读取 MESSAGE 字段,而是直接把字符串常量 "hello class file" 加载出来了。

10. methods_count和methods: 方法表

methods: 4

表示当前类有 4 个方法,这 4 个方法是:

1. TestClassFile()
2. inc()
3. add(int, int)
4. main(String[])

在 Class 文件中,构造方法不叫 TestClassFile,而叫:

<init>

所以实际上是:

<init>
inc
add
main

每个方法的结构都是:

method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

也就是:

方法访问标志
方法名
方法描述符
方法属性

方法真正的字节码在:

Code 属性

10.1 方法1:构造方法

public TestClassFile();
  descriptor: ()V
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #7                  // Field m:I
      10: return

源码中构造方法是:

public TestClassFile() {
}

但因为有实例字段初始化:

private int m = 10;

所以编译器会把它合并进构造方法中,等价于:

public TestClassFile() {
    super();
    this.m = 10;
}

10.2 方法2:inc()

public TestClassFile();
  descriptor: ()V
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #7                  // Field m:I
      10: return

源码中构造方法是:

public TestClassFile() {
}

但因为有实例字段初始化:

private int m = 10;

所以编译器会把它合并进构造方法中,等价于:

public TestClassFile() {
    super();
    this.m = 10;
}

10.3 方法3:add(int,int)

public int add(int, int);
  descriptor: (II)I
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=4, args_size=3
       0: iload_1
       1: iload_2
       2: iadd
       3: istore_3
       4: iload_3
       5: ireturn

源码是:

public int add(int a, int b) {
    int c = a + b;
    return c;
}

10.4 方法4:main(String[] )

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  Code:
    stack=3, locals=3, args_size=1
       0: new           #8                  // class TestClassFile
       3: dup
       4: invokespecial #13                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: iconst_2
      11: invokevirtual #14                 // Method add:(II)I
      14: istore_2
      15: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
      18: ldc           #24                 // String hello class file
      20: invokevirtual #26                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      23: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
      26: iload_2
      27: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V
      30: return

源码是:

public static void main(String[] args) {
    TestClassFile obj = new TestClassFile();
    int result = obj.add(1, 2);
    System.out.println(MESSAGE);
    System.out.println(result);
}