杰瑞科技汇

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

从源代码到执行

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

Java直接引用与符号引用有何区别?-图1
(图片来源网络,侵删)
  1. 编译:将 .java 源文件编译成 .class 字节码文件。
  2. 加载:通过类加载器将 .class 文件中的二进制数据读入内存,并在方法区创建一个 java.lang.Class 对象。
  3. 链接:这是关键阶段,它又分为三个小步骤:
    • 验证:确保 .class 文件的字节流信息符合当前虚拟机的要求,保证不会危害虚拟机自身安全。
    • 准备:为类的静态变量分配内存,并设置其初始零值(注意,不是代码中赋予的值)。
    • 解析:这是“符号引用”和“直接引用”发挥作用的地方,JVM 将常量池中的符号引用替换为直接引用的过程。
  4. 初始化:执行类构造器 <clinit>() 方法,为静态变量赋予正确的初始值。
  5. 使用:创建对象,调用方法等。
  6. 卸载:类不再被使用,从内存中卸载。

符号引用

是什么?

符号引用是一组符号来描述所引用的目标,它和虚拟机的内存布局无关,引用的目标不一定已经加载到内存中,你可以把它想象成在地图上查找一个地址的名称,北京市朝阳区三里屯太古里”,这个名字是明确的,但它不告诉你具体怎么走,也不告诉你这个地方是否存在。

在哪里?

符号引用主要存在于 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 的常量池中,会存在类似这样的符号引用:

Java直接引用与符号引用有何区别?-图2
(图片来源网络,侵删)
  • 对于 String 类型:CONSTANT_Class_infojava/lang/String
  • 对于 System.outCONSTANT_Fieldref_info,它指向一个 CONSTANT_Class_infojava/lang/System)和一个 CONSTANT_NameAndType_infooutLjava/io/PrintStream;)。
  • 对于 println 方法:CONSTANT_Methodref_info,它指向一个 CONSTANT_Class_infojava/io/PrintStream)和一个 CONSTANT_NameAndType_infoprintln()V,表示无参数返回void)。

这些 java/lang/Stringjava/lang/Systemoutprintln 等就是符号引用,它们只是字符串,JVM 在解析之前并不知道它们对应的内存地址是什么。


直接引用

是什么?

直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,它和虚拟机的内存布局直接相关,如果引用的目标存在,那么直接引用就是可以直接使用的内存地址,这就像你已经知道了从当前位置到目的地的具体路线和门牌号,可以直接出发。

在哪里?

直接引用是在类加载的解析阶段,由 JVM 将符号引用转换后得到的,它存在于 JVM 的方法区运行时数据区(如堆)中。

例子

继续上面的例子,在 SymbolReferenceExample 类被加载和链接时:

Java直接引用与符号引用有何区别?-图3
(图片来源网络,侵删)
  1. 当解析 String myString 的类型 java/lang/String 时,JVM 会在方法区找到已经加载的 java.lang.Class<String> 对象,这个对象的内存地址就是 String 类的直接引用
  2. 当解析 System.out 时:
    • JVM 首先找到 java.lang.System 类的 Class 对象。
    • 然后在这个 Class 对象的方法区中查找名为 out 的静态字段。
    • 找到后,out 字值指向堆中的一个 PrintStream 对象实例,这个实例的内存地址就是 out 字段的直接引用
  3. 当解析 println 方法时:
    • JVM 找到 java.io.PrintStream 类的 Class 对象。
    • 在这个 Class 对象的方法区中查找名为 println、参数为 、返回值为 void 的方法。
    • 找到后,这个方法的入口地址(一个指向方法区中具体代码的指针)println 方法的直接引用

一旦解析完成,字节码指令(如 getstatic, invokevirtual)中使用的就不再是这些字符串形式的符号引用,而是这些高效的内存地址(直接引用)。


核心区别总结

特性 符号引用 直接引用
形式 一组符号(如类名、字段名、方法名),存在于常量池。 一个指向内存地址的指针、偏移量或句柄。
与内存布局关系 无关,它不关心目标在内存中的具体位置。 相关,它直接指向目标在 JVM 内存中的实际位置。
存在阶段 主要存在于编译后的 .class 文件中,在解析前被使用。 在类加载的解析阶段生成,在类运行时被使用。
目标状态 引用的目标不一定已经被加载到内存中。 引用的目标必须已经存在于内存中(在解析时已加载)。
好比 地图上的地名(如“天安门广场”)。 具体的 GPS 坐标或详细路线
目的 提供一种与平台无关的、描述目标的方式。 提供一种高效的、可以在运行时直接定位目标的方式。

为什么需要这个转换过程?

这个从符号引用到直接引用的解析过程至关重要,原因如下:

  1. 延迟加载:Java 支持动态加载类,解析过程可以延迟到真正使用该引用时才进行,一个类引用了另一个不常用的类,只有在第一次调用这个类的方法时,JVM 才会去加载并解析那个类,这提高了程序的启动和运行效率。
  2. 内存安全与隔离:在解析阶段,JVM 会验证符号引用的有效性,如果引用的类不存在,或者访问权限不足(比如试图访问一个 private 方法),JVM 会在解析时抛出 IncompatibleClassChangeErrorIllegalAccessError 等异常,从而保证了类的安全性和封装性。
  3. 性能优化:一旦解析完成,后续的指令操作(如获取字段值、调用方法)就变成了直接的内存地址操作,速度极快,避免了每次都要通过字符串去查找的昂贵开销。

符号引用是编译时的“概念”,它用名字告诉 JVM“我想用哪个东西”,而直接引用是运行时的“地址”,它告诉 JVM“这个东西就在这里,直接用这个地址去访问”。

符号引用 -> 解析 -> 直接引用 这个过程,是 JVM 连接编译时信息和运行时内存的桥梁,是 Java 实现其动态性、安全性和高性能的关键一环。

分享:
扫描分享到社交APP
上一篇
下一篇