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

相关推荐
phltxy22 分钟前
C语言操作符详解
java·c语言·算法
步行cgn1 小时前
@Configuration 详解:Spring 配置类的核心注解
java·后端·spring
sunshine22 girl2 小时前
Java学习一 环境配置2 安装和基本使用Idea
java·学习·intellij-idea
嘿嘿-663 小时前
Windows 一键使用 GPT-6 Astra:Codex CLI 配置教程
java·人工智能·windows·gpt·chatgpt·web
策案案案4 小时前
YOLO: 将AI Agents嵌入到IntelliJ IDEA
java·大数据·人工智能·笔记·yolo·microsoft·intellij-idea
辰烨chenye5 小时前
LeetCode Hot 100 题解 · 堆篇
java·算法·leetcode
shehuiyuelaiyuehao6 小时前
算法29,前缀和,除自身以外的数组的乘积
java·数据结构·算法
小刘在重生~6 小时前
Java常用类|String类详解 + BigDecimal精准计算(含面试题)
java·开发语言·python
随遇而安zx6 小时前
【多线程】---JMM 与 volatile 深度解析
java·多线程