APS 系列文章 09

从方法调用到字节码:APS 分发机制逐层拆解

这一篇回答那个从第一篇埋到现在的问题:AcceleratedProxy.invokeSuper(proxy, method, args) 这一行,最后到底变成了什么?为什么它能快到接近直接调用?

答案的关键词是:一条 INVOKESPECIAL 指令

先看慢在哪:反射与 MethodHandle 的边界

在 JDK 动态代理里,拦截器要调原方法,只能 method.invoke(target, args)。这一行的问题在于它每次调用都要付出反射的成本

  • 访问权限检查;
  • 参数装箱成 Object[]
  • 最关键的是,Method.invoke 是 JVM 的内联屏障------JIT 编译器无法把这次调用内联掉,因为目标方法是在运行时才知道的。

MethodHandleMethod.invoke 轻,但本质上仍是「运行时分派」,同样难以被内联到只剩几条机器指令。

APS 的思路很直接:既然方法在生成字节码的那一刻就是已知的,那就别在运行时查找,直接生成一条 INVOKESPECIAL 调用父类方法

生成的代理类长什么样

APS 为每个目标类生成一个子类(或接口实现),结构上就三样东西:

  1. 每个不同的拦截器一个字段_interceptor$0_interceptor$1......
  2. 每个可代理方法一个覆盖方法:负责把参数装箱、调用拦截器。
  3. 一个 dispatch(Method, Object[]) 方法 :根据 Method 分派到对应的父类方法。

调用链路是这样的:

text 复制代码
proxy.hello("World")
   │  覆盖方法 hello(String) 被调用
   ▼
_box args_ → interceptor.intercept(proxy, method, args)
   │  拦截器里调用了 invokeSuper(proxy, method, args)
   ▼
((DispatchTarget) proxy).dispatch(method, args)
   │  按 method.hashCode() 分支
   ▼
super.hello("World")   ← 一条 INVOKESPECIAL,直接调父类

概念上,那个 dispatch 方法生成的字节码等价于这样一段伪代码:

text 复制代码
Object dispatch(Method m, Object[] args) {
    switch (m.hashCode()) {
        case 2102374382:                       // add(int,int) 与 add(int,int,int) 同名
            if (参数是2个) return super.add((Integer)args[0], (Integer)args[1]);
            else            return super.add((Integer)args[0], (Integer)args[1], (Integer)args[2]);
        case 2105388484:                       // name()
            return super.name();
        default:
            throw new NoSuchMethodError();
    }
}

注意最后那几行 return super.add(...)------它们在字节码里就是 INVOKESPECIAL 直接调用父类方法 ,没有反射、没有 MethodHandle。这就是「为什么快」的全部秘密。

一个藏得很深的坑:重载方法的 hashCode 相同

上面伪代码里 add(int,int)add(int,int,int) 落在同一个 case 上,不是巧合。Method.hashCode() 的定义是:

text 复制代码
getName().hashCode() ^ getDeclaringClass().getName().hashCode()

不包含参数类型 !所以同名的重载方法,hashCode() 完全相同。

跑一下就能验证(源码在 examples/.../ch09/):

text 复制代码
name -> hashCode=2105388484
add  -> hashCode=2102374382
add  -> hashCode=2102374382     ← 两个 add 一样!

所以 APS 不能只用 Method.hashCode() 分派重载方法。它的做法分两层:

  1. 先用 Method.hashCode() 分支 ------这一步是零分配 的(hashCode() 是内存操作,不碰 Class[] 数组)。
  2. 只有落在「同名重载」分支上时,才调用 methodDispatchHash,它把参数类型折叠进 hash:
java 复制代码
int methodDispatchHash(Method method) {
    int hash = method.hashCode();
    for (Class<?> pt : method.getParameterTypes()) {   // 只有这里才碰参数类型
        hash = 31 * hash + pt.getName().hashCode();
    }
    return hash;
}

注意它用的是 pt.getName().hashCode() 而不是 pt.hashCode()------Class.hashCode() 依赖对象身份、跨 JVM 不确定,而 Class.getName() 是确定字符串。这个细节保证了分派 hash 在任意 JVM 上结果一致。

结论 :绝大多数方法(没有同名重载的)走的是零分配的 hashCode() 分支;只有重载方法才多一步参数类型折叠。这就是 README 里那句「allocation-free dispatch」的含义。

怎么看生成出来的类

源码在 examples/.../ch09/,运行它可以看到代理类的基本信息:

text 复制代码
proxy class: ...Calc$$AcceleratedProxy$$0/0x0000000037244800
isHidden:    true
dispatch -> public int ...Calc.add(int,int)
3
dispatch -> public int ...Calc.add(int,int,int)
6
dispatch -> public java.lang.String ...Calc.name()
calc

两点观察:

  • 类名带 $$AcceleratedProxy$$ 后缀,是 APS 生成的代理类;
  • isHidden: true 说明它是隐藏类Lookup.defineHiddenClass),这一点下一篇会展开。

想亲眼看到 INVOKESPECIAL,可以加 -Xlog:class+load 或借助 jdk.internal.org.objectweb.asm.util.ASMifier 反汇编生成类,但原理已经说清楚了------拦截器到父类之间,就是一条直接调用。

小结

这一篇把「为什么快」拆到了字节码层:

  1. 反射/MethodHandle 是运行时分派,JIT 内联不了;
  2. APS 把分派提前到字节码生成期dispatch 里是 switch + 直接 INVOKESPECIAL
  3. Method.hashCode() 做零分配分支,methodDispatchHash 折叠参数类型解重载,Class.getName() 保证跨 JVM 确定。

下一篇补上另一半拼图:为什么这条 INVOKESPECIAL 能被 JIT 内联,以及 WeakCache + 隐藏类怎么保证不泄漏内存

本文示例代码见 aps-blog/examples,框架源码与完整文档见 APS 仓库

相关推荐
evans在进步2 小时前
LeetCode 198:打家劫舍——Java 动态规划详解
java·leetcode·动态规划
2601_963870222 小时前
【计算机毕业设计】基于Spring Boot的考研信息交流平台的设计与实现
java·spring boot·后端
大模型丫丫2 小时前
Spring Boot 入门指南:从零开始构建你的第一个应用
java·spring boot·后端
拾光Ծ2 小时前
Linux五种I/O模型与fcntl非阻塞控制
java·linux·服务器·i/o模型
半旧5182 小时前
【Java Web02】WEB服务器内部实现技术揭秘
java·服务器·前端
gis开发之家3 小时前
日志怎么打才专业?Spring Boot 4 日志体系与 Logback 配置(生产级实战)
java·spring boot·后端·logback
SaaS发言人3 小时前
渠道数字化解决方案横评:谁更懂快消
java·大数据·人工智能
JL153 小时前
Java并发编程面试全攻略-从synchronized到AQS底层原理
java·开发语言·面试·并发编程
ZC跨境爬虫3 小时前
LeetCode 119. 杨辉三角 II(原地更新优化详解 + Java Python 实现)
java·python·leetcode