杰瑞科技汇

Java符号引用与直接引用有何区别?

符号引用

定义

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

Java符号引用与直接引用有何区别?-图1
(图片来源网络,侵删)

特点

  • 抽象性:它不指向内存中的具体地址,只是一个逻辑上的名称。
  • 独立性:与 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 类可能还没有被加载、链接和初始化。


直接引用

定义

直接引用是可以直接指向目标的引用,它可以是:

  1. 目标在内存中的指针(如果是已加载的类、方法、字段)。
  2. 相对偏移量(如果是指向实例变量或本地变量的句柄)。
  3. 一个能间接定位到目标的句柄

简单说,直接引用就是 JVM 在运行时可以直接使用的内存地址。

Java符号引用与直接引用有何区别?-图2
(图片来源网络,侵删)

特点

  • 具体性:它指向内存中的具体位置。
  • 依赖性:与 JVM 的内存布局强相关,在不同的虚拟机实现、甚至同一个虚拟机的不同运行时刻,直接引用都可能不同。
  • 存在形式:在类加载的解析阶段,符号引用会被替换成直接引用,并存储在运行时常量池中。
  • 生命周期:主要存在于解析阶段之后的整个运行期间。

例子

当 JVM 解析 MyClass 中的符号引用 java.lang.String 时,它会去检查 java.lang.String 这个类是否已经被加载,如果已经加载,JVM 就会在方法区中为这个类找到它的数据,然后将符号引用替换为指向这个类数据的内存指针,这个内存指针就是直接引用。


核心区别与对比

特性 符号引用 直接引用
定义 一组符号(如全限定名)来描述目标 可以直接指向目标的指针、偏移量或句柄
形式 抽象的、逻辑上的名称 具体的、物理上的内存地址
依赖性 与 JVM 内存布局无关 与 JVM 内存布局强相关
存在阶段 编译后、类加载的解析阶段之前 类加载的解析阶段之后、运行时
目标状态 目标可能还未加载到内存 目标必须已经在内存中(已被加载)
好比 通讯录里的“张三的电话” 你手机里存储的“张三”的号码 ..

它们之间的关系:类加载过程

符号引用和直接引用的转换,发生在类加载过程的链接阶段中的解析步骤。

让我们通过一个完整的流程来理解:

编译阶段

你编写 MyClass.java 代码,并使用 javac 编译它。

Java符号引用与直接引用有何区别?-图3
(图片来源网络,侵删)
// 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),然后尝试在内存中找到它们对应的实体。
      1. JVM 发现需要解析 java/util/ArrayList
      2. 检查 ArrayList 是否已经被加载,如果没有,先加载它。
      3. ArrayList 加载、链接、初始化完成后,它在方法区有了自己的数据。
      4. JVM 将 MyClass 常量池中的符号引用 java/util/ArrayList 替换为指向 ArrayList 类对象的直接引用(内存指针)
      5. 同样,JVM 也会解析 add(java/lang/Object) 方法,将其符号引用替换为指向 ArrayList 类中 add 方法的直接引用(方法指针)。

解析完成后,符号引用就变成了直接引用。

初始化阶段

执行 MyClass 类的静态代码块(如果有的话),并给静态变量赋予正确的初始值。

使用阶段

main 方法开始执行。 new java.util.ArrayList() 这行代码,JVM 已经可以通过直接引用快速找到 ArrayList 的构造方法并执行。 list.add(...) 这行代码,JVM 也可以通过直接引用快速找到 ArrayListadd 方法并执行。


为什么要有两种引用?

这种设计是 Java 一次编写,到处运行的关键体现。

  1. 平台无关性:符号引用不依赖任何具体的内存布局,无论你在 Windows、Linux 还是 macOS 上编译代码,生成的 .class 文件里的符号引用都是一样的,这使得 Java 代码可以轻松地在不同平台间移植。
  2. 灵活性:在类加载的早期(如链接之前),JVM 只需要知道“存在”一个目标,而不需要关心它具体在哪里,这为延迟加载、动态链接等高级特性提供了基础。
  3. 安全性:在解析阶段,JVM 可以对符号引用进行验证,确保它指向的目标是合法的、可访问的,这有助于增强安全性。
  • 符号引用是编译时的“名字”,是跨平台的逻辑表示。
  • 直接引用是运行时的“地址”,是特定于 JVM 实现的物理表示。
  • 解析就是将“名字”替换成“地址”的过程,这个转换发生在类加载期间,是连接符号世界和物理世界的关键桥梁。
分享:
扫描分享到社交APP
上一篇
下一篇