从源代码到执行
我们回顾一下 Java 代码从编写到执行的完整流程,这两个概念就发生在链接阶段。

- 编译:将
.java源文件编译成.class字节码文件。 - 加载:通过类加载器将
.class文件中的二进制数据读入内存,并在方法区创建一个java.lang.Class对象。 - 链接:这是关键阶段,它又分为三个小步骤:
- 验证:确保
.class文件的字节流信息符合当前虚拟机的要求,保证不会危害虚拟机自身安全。 - 准备:为类的静态变量分配内存,并设置其初始零值(注意,不是代码中赋予的值)。
- 解析:这是“符号引用”和“直接引用”发挥作用的地方,JVM 将常量池中的符号引用替换为直接引用的过程。
- 验证:确保
- 初始化:执行类构造器
<clinit>()方法,为静态变量赋予正确的初始值。 - 使用:创建对象,调用方法等。
- 卸载:类不再被使用,从内存中卸载。
符号引用
是什么?
符号引用是一组符号来描述所引用的目标,它和虚拟机的内存布局无关,引用的目标不一定已经加载到内存中,你可以把它想象成在地图上查找一个地址的名称,北京市朝阳区三里屯太古里”,这个名字是明确的,但它不告诉你具体怎么走,也不告诉你这个地方是否存在。
在哪里?
符号引用主要存在于 Class 文件的常量池中,它以 CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info 等形式存在。
例子
假设有以下 Java 代码:
public class SymbolReferenceExample {
// 引用 java.lang.String 类
private String myString = "hello";
// 调用 java.lang.System.out.println() 方法
public void printSomething() {
System.out.println("This is a test.");
}
}
编译后,在 SymbolReferenceExample.class 的常量池中,会存在类似这样的符号引用:

- 对于
String类型:CONSTANT_Class_info是java/lang/String。 - 对于
System.out:CONSTANT_Fieldref_info,它指向一个CONSTANT_Class_info(java/lang/System)和一个CONSTANT_NameAndType_info(out和Ljava/io/PrintStream;)。 - 对于
println方法:CONSTANT_Methodref_info,它指向一个CONSTANT_Class_info(java/io/PrintStream)和一个CONSTANT_NameAndType_info(println和()V,表示无参数返回void)。
这些 java/lang/String、java/lang/System、out、println 等就是符号引用,它们只是字符串,JVM 在解析之前并不知道它们对应的内存地址是什么。
直接引用
是什么?
直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,它和虚拟机的内存布局直接相关,如果引用的目标存在,那么直接引用就是可以直接使用的内存地址,这就像你已经知道了从当前位置到目的地的具体路线和门牌号,可以直接出发。
在哪里?
直接引用是在类加载的解析阶段,由 JVM 将符号引用转换后得到的,它存在于 JVM 的方法区和运行时数据区(如堆)中。
例子
继续上面的例子,在 SymbolReferenceExample 类被加载和链接时:

- 当解析
String myString的类型java/lang/String时,JVM 会在方法区找到已经加载的java.lang.Class<String>对象,这个对象的内存地址就是String类的直接引用。 - 当解析
System.out时:- JVM 首先找到
java.lang.System类的Class对象。 - 然后在这个
Class对象的方法区中查找名为out的静态字段。 - 找到后,
out字值指向堆中的一个PrintStream对象实例,这个实例的内存地址就是out字段的直接引用。
- JVM 首先找到
- 当解析
println方法时:- JVM 找到
java.io.PrintStream类的Class对象。 - 在这个
Class对象的方法区中查找名为println、参数为 、返回值为void的方法。 - 找到后,这个方法的入口地址(一个指向方法区中具体代码的指针)
println方法的直接引用。
- JVM 找到
一旦解析完成,字节码指令(如 getstatic, invokevirtual)中使用的就不再是这些字符串形式的符号引用,而是这些高效的内存地址(直接引用)。
核心区别总结
| 特性 | 符号引用 | 直接引用 |
|---|---|---|
| 形式 | 一组符号(如类名、字段名、方法名),存在于常量池。 | 一个指向内存地址的指针、偏移量或句柄。 |
| 与内存布局关系 | 无关,它不关心目标在内存中的具体位置。 | 相关,它直接指向目标在 JVM 内存中的实际位置。 |
| 存在阶段 | 主要存在于编译后的 .class 文件中,在解析前被使用。 |
在类加载的解析阶段生成,在类运行时被使用。 |
| 目标状态 | 引用的目标不一定已经被加载到内存中。 | 引用的目标必须已经存在于内存中(在解析时已加载)。 |
| 好比 | 地图上的地名(如“天安门广场”)。 | 具体的 GPS 坐标或详细路线。 |
| 目的 | 提供一种与平台无关的、描述目标的方式。 | 提供一种高效的、可以在运行时直接定位目标的方式。 |
为什么需要这个转换过程?
这个从符号引用到直接引用的解析过程至关重要,原因如下:
- 延迟加载:Java 支持动态加载类,解析过程可以延迟到真正使用该引用时才进行,一个类引用了另一个不常用的类,只有在第一次调用这个类的方法时,JVM 才会去加载并解析那个类,这提高了程序的启动和运行效率。
- 内存安全与隔离:在解析阶段,JVM 会验证符号引用的有效性,如果引用的类不存在,或者访问权限不足(比如试图访问一个
private方法),JVM 会在解析时抛出IncompatibleClassChangeError或IllegalAccessError等异常,从而保证了类的安全性和封装性。 - 性能优化:一旦解析完成,后续的指令操作(如获取字段值、调用方法)就变成了直接的内存地址操作,速度极快,避免了每次都要通过字符串去查找的昂贵开销。
符号引用是编译时的“概念”,它用名字告诉 JVM“我想用哪个东西”,而直接引用是运行时的“地址”,它告诉 JVM“这个东西就在这里,直接用这个地址去访问”。
符号引用 -> 解析 -> 直接引用 这个过程,是 JVM 连接编译时信息和运行时内存的桥梁,是 Java 实现其动态性、安全性和高性能的关键一环。
