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 仓库。

相关推荐
天天被压力4 分钟前
【Python 量化取数指南 #13】Python 把行情落库:sqlite 一键存,回测随用随取
java·人工智能·python
天天被压力4 分钟前
【Python 量化取数指南 #14】Python 清洗行情数据:复权停牌对齐,回测不翻车
java·人工智能·python
独泪了无痕8 分钟前
开发利器Hutool之MapUtil的使用
java·后端
唐青枫12 分钟前
Java ThreadLocal 详解:从线程隔离到线程池实战,彻底避开脏数据与内存泄漏
java
T_Apollo12 分钟前
2.6 Java 11 版本新增特性和类
java
用户37215742613521 分钟前
如何使用 Java 创建 Excel 数据透视表
java
用户31268748772026 分钟前
JVM 内存结构全链路:堆、栈、方法区、元空间
java
IT枫斗者枫哥26 分钟前
Spring Boot导入返回409,为什么第一行还是入库了?
java·spring boot·后端
xiaoqiMikko26 分钟前
JVM 线上排查实战(五):升 JDK 17 后进程起不来,十几个老 GC 参数挨个实测(完结)
java·jvm
Flynt27 分钟前
Java 27 悄悄改了 3 个默认值,我在 1 核小机器上逐个验证了一遍
java·jvm·性能优化