符号引用
定义
符号引用是一组符号来描述所引用的目标,它和虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中,你可以把它想象成在程序代码中使用的“名字”或“代号”。

特点
- 抽象性:它不指向内存中的具体地址,只是一个逻辑上的名称。
- 独立性:与 JVM 的内存布局无关,无论在哪个平台,符号引用的形式都是一样的。
- 存在形式:通常以全限定名的形式存在于 Class 文件的常量池中。
- 一个类的全限定名:
java.lang.String - 一个字段的全限定名:
java.io.PrintStream.println - 一个方法的全限定名:
java.lang.String.substring(int, int)
- 一个类的全限定名:
- 生命周期:主要存在于编译阶段和类加载的解析阶段之前。
例子
在你的 Java 代码中写下这样一行:
String str = new java.lang.String();
编译后,在生成的 MyClass.class 文件的常量池里,会有一条符号引用指向 java.lang.String 这个类,当 JVM 加载 MyClass 时,它首先会看到这个符号引用,但此时 java.lang.String 类可能还没有被加载、链接和初始化。
直接引用
定义
直接引用是可以直接指向目标的引用,它可以是:
- 目标在内存中的指针(如果是已加载的类、方法、字段)。
- 相对偏移量(如果是指向实例变量或本地变量的句柄)。
- 一个能间接定位到目标的句柄。
简单说,直接引用就是 JVM 在运行时可以直接使用的内存地址。

特点
- 具体性:它指向内存中的具体位置。
- 依赖性:与 JVM 的内存布局强相关,在不同的虚拟机实现、甚至同一个虚拟机的不同运行时刻,直接引用都可能不同。
- 存在形式:在类加载的解析阶段,符号引用会被替换成直接引用,并存储在运行时常量池中。
- 生命周期:主要存在于解析阶段之后的整个运行期间。
例子
当 JVM 解析 MyClass 中的符号引用 java.lang.String 时,它会去检查 java.lang.String 这个类是否已经被加载,如果已经加载,JVM 就会在方法区中为这个类找到它的数据,然后将符号引用替换为指向这个类数据的内存指针,这个内存指针就是直接引用。
核心区别与对比
| 特性 | 符号引用 | 直接引用 |
|---|---|---|
| 定义 | 一组符号(如全限定名)来描述目标 | 可以直接指向目标的指针、偏移量或句柄 |
| 形式 | 抽象的、逻辑上的名称 | 具体的、物理上的内存地址 |
| 依赖性 | 与 JVM 内存布局无关 | 与 JVM 内存布局强相关 |
| 存在阶段 | 编译后、类加载的解析阶段之前 | 类加载的解析阶段之后、运行时 |
| 目标状态 | 目标可能还未加载到内存 | 目标必须已经在内存中(已被加载) |
| 好比 | 通讯录里的“张三的电话” | 你手机里存储的“张三”的号码 .. |
它们之间的关系:类加载过程
符号引用和直接引用的转换,发生在类加载过程的链接阶段中的解析步骤。
让我们通过一个完整的流程来理解:
编译阶段
你编写 MyClass.java 代码,并使用 javac 编译它。

// MyClass.java
public class MyClass {
public static void main(String[] args) {
java.util.ArrayList list = new java.util.ArrayList();
list.add("Hello");
}
}
编译后,MyClass.class 文件生成,在它的常量池中,会包含以下符号引用:
java/util/ArrayList(类)add(java/lang/Object)(方法)
只有符号引用。
类加载阶段
当运行 java MyClass 时,JVM 的类加载器开始工作。
- 加载:将
MyClass.class文件读入内存,并生成一个java.lang.Class对象。 - 链接:
- 验证:检查 Class 文件格式是否正确。
- 准备:为类的静态变量分配内存,并设置零值。
- 解析:这是关键步骤。
JVM 遍历
MyClass的常量池,找到那些符号引用(如java/util/ArrayList),然后尝试在内存中找到它们对应的实体。- JVM 发现需要解析
java/util/ArrayList。 - 检查
ArrayList是否已经被加载,如果没有,先加载它。 ArrayList加载、链接、初始化完成后,它在方法区有了自己的数据。- JVM 将
MyClass常量池中的符号引用java/util/ArrayList替换为指向ArrayList类对象的直接引用(内存指针)。 - 同样,JVM 也会解析
add(java/lang/Object)方法,将其符号引用替换为指向ArrayList类中add方法的直接引用(方法指针)。
- JVM 发现需要解析
解析完成后,符号引用就变成了直接引用。
初始化阶段
执行 MyClass 类的静态代码块(如果有的话),并给静态变量赋予正确的初始值。
使用阶段
main 方法开始执行。
new java.util.ArrayList() 这行代码,JVM 已经可以通过直接引用快速找到 ArrayList 的构造方法并执行。
list.add(...) 这行代码,JVM 也可以通过直接引用快速找到 ArrayList 的 add 方法并执行。
为什么要有两种引用?
这种设计是 Java 一次编写,到处运行的关键体现。
- 平台无关性:符号引用不依赖任何具体的内存布局,无论你在 Windows、Linux 还是 macOS 上编译代码,生成的
.class文件里的符号引用都是一样的,这使得 Java 代码可以轻松地在不同平台间移植。 - 灵活性:在类加载的早期(如链接之前),JVM 只需要知道“存在”一个目标,而不需要关心它具体在哪里,这为延迟加载、动态链接等高级特性提供了基础。
- 安全性:在解析阶段,JVM 可以对符号引用进行验证,确保它指向的目标是合法的、可访问的,这有助于增强安全性。
- 符号引用是编译时的“名字”,是跨平台的逻辑表示。
- 直接引用是运行时的“地址”,是特定于 JVM 实现的物理表示。
- 解析就是将“名字”替换成“地址”的过程,这个转换发生在类加载期间,是连接符号世界和物理世界的关键桥梁。
