APS 系列文章10

为什么能被 JIT 内联,又为什么 GC 安全

上一篇拆到了「dispatch 里是一条 INVOKESPECIAL」。这一篇补上剩下的两半拼图:为什么这条指令能被 JIT 内联 ,以及 APS 怎么保证生成一堆代理类后不会内存泄漏

第一半:为什么 INVOKESPECIAL 能被内联

JIT 编译器做方法内联的前提是:在调用点,它要知道目标方法是谁

  • INVOKESPECIAL(调父类/私有方法)、INVOKEVIRTUAL(虚方法)这类调用,目标方法在编译期就是确定或可静态解析的,JIT 能直接把方法体「搬」进调用点,进一步优化(常量折叠、逃逸分析、甚至把整个调用消除)。
  • Method.invoke 是反射:它是个 native 方法,真正要调的目标方法要到运行时 才从 Method 对象里解析出来。JIT 看不到目标,只能眼睁睁看着它变成一次完整的反射调用------这就是内联屏障。
  • MethodHandle 介于两者之间:靠 invokedynamic 的 signature polymorphism 能内联到 LambdaForm,但多一层间接,稳定性不如直接调用。

所以 APS 的核心优势可以一句话总结:它把「运行时分派」变成了「编译期已知的直接调用」 。拦截器到父类之间那条 INVOKESPECIAL,就是 JIT 最喜欢内联的那种东西。这也是为什么基准里 APS 的透传能贴着直接调用跑。

第二半:隐藏类与 WeakCache 的 GC 安全

动态代理最经典的内存问题是 ClassLoader 泄漏

老 CGLib 用 defineClass 把生成的代理类挂到目标类的加载器上。一旦你的应用反复热部署、反复生成代理类,这些类会一直挂在加载器上 ,加载器又引用着类,类又引用着加载器......形成一个 GC 不掉的环,Metaspace(或老版的 PermGen)越涨越高,最终 OutOfMemoryError

APS 用了两招规避:

1. Lookup.defineHiddenClass 定义隐藏类

APS 用 Lookup.defineHiddenClass 生成代理类。隐藏类不注册到任何 ClassLoader 的类定义表里 ------它不是「加载器拥有的一组类」的一员,而是和定义它的 Lookup 松散关联。当代理实例不再被引用,隐藏类就能被 GC 回收,不会拖着加载器不放。

看代理类对象的两个属性就能验证:

text 复制代码
proxy is hidden:   true
proxy class name:  ...Greeter$$AcceleratedProxy$$0

isHidden()true,说明它就是个隐藏类。

2. WeakCache 缓存复用,而不是无限生成

APS 把生成的代理类缓存在一个 WeakCache 里,缓存键是「方法 → 拦截器映射」。两个效果:

  • 复用 :同一目标类 + 同样的拦截器映射,只生成一次,反复 proxy(...) 拿到的是同一个类。实测 a.getClass() == b.getClass()true
  • 可回收WeakCache 用弱引用持键,映射没了、没有实例引用时,缓存项可以被 GC 清掉,不会无限堆积。

这也顺带解释了 evict(第七篇)的语义------它只是把缓存项手动踢掉,让下次 proxy(...) 重新生成。

完整演示

源码在 examples/src/main/java/io/github/lamspace/blog/ch10/

java 复制代码
public class Main {
    public static class Greeter {
        public String hello(String name) { return "Hello, " + name; }
    }

    public static void main(String[] args) {
        Greeter a = AcceleratedProxy.proxy(Greeter.class,
                (o, m, ar) -> AcceleratedProxy.invokeSuper(o, m, ar));
        Greeter b = AcceleratedProxy.proxy(Greeter.class,
                (o, m, ar) -> AcceleratedProxy.invokeSuper(o, m, ar));
        System.out.println("a.class == b.class ? " + (a.getClass() == b.getClass()));

        System.out.println("proxy is hidden:   " + a.getClass().isHidden());
        System.out.println("proxy class name:  " + a.getClass().getName());

        AcceleratedProxy.evict(Greeter.class);
        Greeter c = AcceleratedProxy.proxy(Greeter.class,
                (o, m, ar) -> AcceleratedProxy.invokeSuper(o, m, ar));
        System.out.println("after evict, a.class == c.class ? "
                + (a.getClass() == c.getClass()));
    }
}

运行输出:

text 复制代码
a.class == b.class ? true
proxy is hidden:   true
proxy class name:  ...Greeter$$AcceleratedProxy$$0
after evict, a.class == c.class ? false

三行分别对应:缓存复用 (true)、隐藏类 (true)、evict 后重新生成(false)。

小结

APS 快、且不泄漏,靠的是两个 JVM 层面的选择:

  • 直接 INVOKESPECIAL 换掉反射/MethodHandle,让 JIT 能内联;
  • 隐藏类 + WeakCache 换掉 defineClass,让代理类可被 GC。

这两点合起来,才是它敢在 README 里写「near-native performance」的底气。

下一篇是本系列的收尾:把 APS、CGLib、java.lang.reflect.Proxy 拉上 JMH 实测,顺便讲讲基准测试的方法学------为什么「手写个 for 循环计时」不可信。

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

相关推荐
不可求~44 分钟前
C++ std::string_view 不是字符串:从悬空引用到安全用法
java·开发语言·c++
考虑考虑1 小时前
Excel导入时产生特殊字符处理
java·后端·java ee
Scabbards_2 小时前
面试Leetcode - Heap 堆
java·leetcode·面试
程序员清风2 小时前
专业再升级!程序员专属显示器明基RD280UG上手实测!
java·后端·面试
Lam Tang4 小时前
APS 系列文章 08
java·代理模式
萧瑟余晖5 小时前
Java深入解析篇三十三之消息队列
java·开发语言
码匠许师傅5 小时前
【C++ 面试真题】聊聊 C++ 的序列容器
java·c++·面试
IT爱学堂5 小时前
尚硅谷Java+AI大模型应用开发革新版本 2025年3月
java·开发语言·人工智能
不爱编程的小九九6 小时前
小九源码-springboot004-springboot智能阅读推荐系统
java·spring boot·后端