为什么能被 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 仓库。