动态链接能干什么?多态时有大用!将符号引用转化为直接引用!

194 阅读4分钟

符号引用:名字而已,还没“实物”

先说“符号引用”(Symbolic Reference),这玩意儿其实就是个“名字”或者“代号”。在 Java 代码编译成字节码的时候,JVM 不会直接把所有的地址、内存位置啥的都写死,而是用一些符号来占位。这些符号指向常量池里的东西,比如类名、方法名、字段名啥的。

举个例子:

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

你编译这段代码,生成的字节码里,System.out.println 不会直接写成内存地址,而是用一个符号引用,比如 #2,指向常量池里的条目。常量池里可能是这样的:

#2 = Methodref java/io/PrintStream.println:(Ljava/lang/String;)V

这个 #2 就是符号引用,意思是“嘿,我要调用 java.io.PrintStreamprintln 方法,参数是字符串,返回 void”。但这时候,JVM 还不知道 PrintStream 的具体地址在哪儿,符号引用只是个抽象的描述。

特点

  • 它是静态的,存在于字节码文件里。
  • 不涉及运行时的实际内存地址。
  • 需要解析(resolve)才能用。

直接引用:实打实的“地址”

再说“直接引用”(Direct Reference),这就好比从“名字”变成了“门牌号”。符号引用经过解析后,JVM 会把它变成一个具体的指针或者偏移量,直接指向内存里类、方法或者字段的实际位置。

还是上面的例子,#2 这个符号引用在运行时被解析后,可能会变成一个指向 PrintStream.println 方法的内存地址,比如 0x7f8b1234(当然这是假的,具体实现因 JVM 不同而异)。这个地址就是直接引用,JVM 用它就能立刻找到目标,不用再查表或者瞎猜。

特点

  • 它是动态的,运行时生成的。
  • 直接跟内存挂钩,效率高。
  • 解析完符号引用后就有了。

动态链接:从名字到地址的桥梁

那“动态链接”(Dynamic Linking)是啥呢?它就是把符号引用变成直接引用的过程,而且这个过程是在运行时动态完成的。JVM 的栈帧里有个“动态链接”部分,专门负责处理这种转换。

为啥叫“动态”?因为 Java 支持多态、接口、虚方法这些特性,具体调用哪个方法,可能得等到运行时才知道。比如:

interface Animal {
    void speak();
}

class Dog implements Animal {
    public void speak() {
        System.out.println("Woof");
    }
}

class Cat implements Animal {
    public void speak() {
        System.out.println("Meow");
    }
}

class Test {
    void makeSpeak(Animal a) {
        a.speak();
    }
}

你调用 makeSpeak(new Dog())makeSpeak(new Cat())a.speak() 的符号引用在字节码里可能是 #3 = InterfaceMethodref Animal.speak:()V。但具体调 Dog.speak 还是 Cat.speak,得看运行时传进来的对象是啥。动态链接这时候就上场了:

  1. 看到符号引用 #3
  2. 检查 a 的实际类型(比如 Dog)。
  3. 解析成 Dog.speak 的直接引用(比如内存地址 0x7f8b5678)。
  4. 执行。

作用

  • 支持运行时绑定,解决多态问题。
  • 把静态的符号引用变成可执行的直接引用。
  • 跟常量池和方法区协作完成解析。

三者关系:从静态到动态的旅程

简单总结下:

  • 符号引用是起点,存在于字节码里,像个地图上的地名。
  • 直接引用是终点,指向内存里的具体位置,像个 GPS 坐标。
  • 动态链接是过程,把地名变成坐标的导航仪。

在 JVM 里,动态链接发生在类加载的链接阶段(具体是解析阶段,Resolution),或者第一次执行某个方法调用时(延迟解析,懒加载)。栈帧里的动态链接部分会记录这些符号引用的状态,确保方法跑起来时能找到正确的目标。


一个例子串起来

再拿个例子理一遍:

class Demo {
    public static void main(String[] args) {
        System.out.println("Hi");
    }
}
  1. 字节码里invokevirtual #4,常量池 #4 = Methodref java/io/PrintStream.println:(Ljava/lang/String;)V,这就是符号引用。
  2. 运行时:JVM 加载 PrintStream 类,找到 println 方法的实际地址(比如 0x12345678),这就是直接引用。
  3. 动态链接:栈帧里的动态链接把 #4 解析成 0x12345678,然后操作数栈把 "Hi" 压进去,调用顺利完成。

最后唠两句

符号引用、直接引用和动态链接,本质上是 JVM 在静态字节码和动态运行之间搭的桥。多亏了动态链接,Java 才能玩转多态和灵活性。要不然,代码全写死成地址,哪还有啥扩展性可言啊,对吧?