5.2 加载、链接与调用:从符号引用到 invokedynamic
一条字节码调用指令走进运行时后,究竟落到哪个类、哪个方法和哪段机器码,取决于加载与分派过程。
字节码里的方法调用通常不是某个机器地址,而是常量池中的符号引用。JVM 要把名字和描述符解析为可调用目标,同时遵守类加载器、访问控制和动态分派规则。
加载、链接、初始化不是一件事
类或接口的生命周期可以先按三段理解:
加载 Loading
↓ 读取二进制表示,创建 Class 对象
链接 Linking
├─ 验证 Verification
├─ 准备 Preparation
└─ 解析 Resolution(可按规范允许延后)
初始化 Initialization
↓ 执行类或接口初始化方法 <clinit>准备阶段为静态字段创建存储并设置规范定义的初始值,不等于执行源码里的所有静态初始化表达式。源码中的静态字段赋值和静态块通常被编译进 <clinit>,在初始化阶段执行。
final class Settings {
static int retries = loadRetries();
static int loadRetries() {
System.out.println("initialize");
return 3;
}
}仅仅在 classpath 上发现 Settings.class 不会必然打印;满足主动使用条件触发初始化时才执行 <clinit>。数组类、接口初始化和编译期常量还有更细规则,应以 JLS/JVMS 为准,而不是用“一引用就初始化”概括。
类的身份包含定义它的加载器
JVM 中运行时类型身份不只由二进制类名决定,还与定义它的类加载器有关。两个加载器分别定义名为 com.example.Plugin 的类,它们是不同运行时类型,即使字节完全相同。
这解释了插件系统和应用服务器里常见的异常:
com.example.Plugin cannot be cast to com.example.Plugin错误信息中的两个名字一样,但定义加载器不同。诊断时要同时查看类名、加载来源和加载器层次。
类加载器还决定命名空间隔离和依赖可见性。随意改变线程上下文类加载器,或让插件持有宿主加载器中的静态引用,可能造成类无法卸载和元数据长期占用。
五条调用指令各自表达什么
| 指令 | 典型用途 | 是否由调用点动态选择目标 |
|---|---|---|
invokestatic | 静态方法 | 否 |
invokespecial | 实例初始化、父类调用等特殊分派 | 按特殊规则 |
invokevirtual | 类实例方法 | 是 |
invokeinterface | 接口方法 | 是 |
invokedynamic | 由引导方法建立调用点 | 由调用点协议决定 |
源码语法与指令并非永远一对一。编译器版本、目标 class 版本以及访问方式都可能影响生成结果,所以应以 javap 输出验证具体代码。
invokevirtual:按接收者运行时类型分派
interface Printer {
void print(String text);
}
void run(ConsolePrinter printer) {
printer.print("ready");
}若调用点的符号引用是类方法,通常使用 invokevirtual。解析先确定符号引用是否合法,真正调用时再按接收者实际类选择覆盖实现。
invokeinterface:从接口引用调用
void run(Printer printer) {
printer.print("ready");
}调用点以接口方法引用表示时通常使用 invokeinterface。接口默认方法、冲突选择等语义由规范定义,不能简化成“总是查一张固定接口表”。具体 JVM 可采用多种缓存与优化策略。
invokespecial:绕开普通虚分派规则
构造器 <init> 只能由 invokespecial 调用。显式父类方法调用等场景也使用特殊选择规则:
class Child extends Parent {
Child() {
super();
}
void refresh() {
super.refresh();
}
}不要记成“所有 private 方法必然对应 invokespecial”。源码访问级别和最终指令的关系经历过演进,也受具体调用形态影响;判断某段编译产物仍应查看字节码。
invokedynamic 把链接策略交给引导方法
前四条指令的链接语义主要由 JVM 预先规定。invokedynamic 的常量池项还指向一个引导方法及静态参数。调用点第一次链接时,引导方法返回一个 CallSite,其中的目标 MethodHandle 决定后续调用行为。
invokedynamic 指令
↓ 首次链接
bootstrap method + 名称 + 方法类型 + 静态参数
↓
CallSite(target MethodHandle)
↓
后续调用目标它不是“每次调用都重新反射查找”。建立调用点后,JVM 可以按 CallSite 类型和实现策略优化。
Lambda 与字符串拼接只是常见用法
Function<String, Integer> length = String::length;
String message = "user=" + userId + ", count=" + count;现代 javac 常用 invokedynamic 配合引导方法实现 Lambda 和非常量字符串拼接。常量表达式如:
String value = "hello" + "world";可能在编译期直接折叠为常量。
这些是编译器生成策略,不是说 Java 语言规范要求所有 Lambda 必须以某个固定引导方法实现。其他 JVM 语言还可用 invokedynamic 实现动态方法分派、运行时适配等不同语义。
用下面的命令寻找调用点和 BootstrapMethods 属性:
javac LambdaDemo.java
javap -c -v -p LambdaDemo重点观察:
InvokeDynamic常量池项的名称和方法描述符;BootstrapMethods属性;- 引导方法句柄及静态参数;
- 编译器是否生成了额外的合成方法。
完成检查
写一个类,包含静态方法、实例覆盖、接口调用、super 调用、Lambda 和字符串拼接。然后:
- 标出五种调用指令实际出现了哪些;
- 对每个调用点解释接收者从哪里进入操作数栈;
- 找到一个
invokedynamic的引导方法; - 修改变量的静态类型,观察调用指令是否变化;
- 不运行程序,先预测哪些操作会触发类初始化,再用日志验证。