Java 动态代理主流两种实现:JDK 动态代理、CGLIB 动态代理,Spring 5.x + 还有 CGLIB 的替代Objenesis,另外字节码生成、Lambda 代理、ByteBuddy 也属于广义动态代理。动态代理本质:运行期生成代理类 / 代理对象,拦截目标方法,做增强(AOP、RPC、埋点、事务)。
核心结论:动态代理有开销,但绝大多数业务场景下不是瓶颈;热点路径、超高 QPS、极小方法、循环内频繁创建代理对象时,性能问题会被放大。
一、两种代理底层原理简要回顾
JDK 动态代理
基于接口;运行时 ASM 生成
.class字节码,内存中生成代理类,实现目标全部接口;调用走
InvocationHandler.invoke(),所有方法统一进入这一个拦截方法。
CGLIB(Code Generation Library)
基于继承;运行时生成目标类的子类,重写非 final 方法;
调用走
MethodInterceptor.intercept()。
SpringBoot2 默认 AOP:目标有接口优先 JDK,无接口用 CGLIB;SpringBoot 可强制 cglib。

二、性能开销拆解(四大维度)
动态代理性能分为两个完全不同阶段:代理对象创建阶段、代理方法调用阶段,很多人混淆两者。
1)代理对象创建开销(一次性开销)
重点:创建代理很慢,调用代理很快。
JDK / CGLIB 都需要运行时字节码生成、类加载、验证、链接(defineClass)。
字节码生成、类加载是重量级操作,涉及 JVM 内部锁、字节码校验。
✅正确用法:只初始化一次代理对象,复用代理对象
❌反模式:循环、每次请求里 newProxyInstance /create 生成代理。这会带来巨大性能损耗,大量 GC、类元空间 Metaspace 上涨。
Metaspace 风险:频繁动态生成代理类,会产生大量动态类,Metaspace 占用持续上涨,如果类卸载不及时,引发 Metaspace OOM。 CGLIB 老版本尤其容易产生大量动态类;新版本以及 ByteBuddy 做了优化。
2)方法调用开销(每次方法执行,高频路径)
代理对象创建完成之后,后续方法调用的开销。 调用链路:业务代码 → 代理类方法 → 拦截器 → 反射 / FastClass → 目标方法。
JDK 动态代理调用链路
proxy.method() → 代理类方法硬编码调用 InvocationHandler.invoke(proxy, method, args)
入参:
Method反射对象、Object [] 参数数组。每次调用会装箱参数为 Object [] 数组:基础类型会自动装箱拆箱,产生临时对象,触发 GC。
每次传入
java.lang.reflect.Method实例。
CGLIB 调用链路
老 CGLIB:重写子类方法 → MethodInterceptor.intercept (obj,method,args,methodProxy)
MethodProxy:CGLIB 的 FastClass 机制,绕过反射,直接索引调用目标方法,这是 CGLIB 调用比 JDK 快的关键点。
FastClass:运行期额外生成 FastClass 字节码,用数字索引直接定位方法,避免反射调用开销。
性能对比(基准测试经验值)
原生直接调用作为基准 1 倍
原生方法调用:1x
CGLIB(MethodProxy FastClass):~1.5‑2x,开销很小
JDK 动态代理:~2‑4x;主要开销:Object [] 参数数组、反射 Method 对象
原生反射 Method.invoke:~5‑10x,开销更大
⚠️注意:以上是极小空方法压测数据;如果业务方法本身逻辑很重(IO、DB),代理那几倍开销完全可以忽略不计。只有方法本身执行极快(纳秒‑微秒级),代理开销占比才会凸显。
例子:一个方法执行 10ms,代理额外增加几十 ns,完全无感; 一个方法执行几十 ns,代理增加几十 ns,耗时直接翻倍。
3)GC 开销
JDK 代理每次调用创建
Object[] args数组;基本类型参数会装箱,产生大量短命小对象,YGC 频率上升。动态生成的代理类、FastClass 类,会生成 Class 对象;如果代理对象回收,类不一定马上卸载,依赖类加载器是否可卸载。
短生命周期类加载器 + 频繁创建代理:Metaspace 持续占用。
4)JIT 优化受影响
JVM JIT 会做内联、逃逸分析、消除装箱等优化。动态代理会一定程度干扰 JIT:
JDK 代理入口是统一 invoke 方法,多个不同方法全部走到同一个 invoke,虚方法多,JIT 内联难度上升。
CGLIB 生成子类,多一层继承,虚调用;但 FastClass 直接索引调用,JIT 优化效果要好于 JDK 代理。
拦截器内写复杂逻辑,会阻止 JIT 把目标方法内联。
注意:JIT 预热很关键。刚启动的时候代理调用很慢,JIT 编译完后性能大幅提升;压测要考虑预热。
三、JDK 代理 vs CGLIB 性能总结表
现代 JDK 版本(JDK17+)JDK 动态代理底层做过优化,两者差距已经缩小不少。
四、常见误区
❌误区:CGLIB 一定全面优于 JDK 代理
创建代理对象 CGLIB 更慢;只有调用频繁的时候 CGLIB 优势体现。如果代理只调用很少几次,JDK 反而更好。
❌误区:动态代理性能很差,不要用
AOP、Spring 事务全部基于动态代理;绝大多数业务 IO 密集场景,代理开销可以忽略。不要过早优化。
❌误区:每次请求创建代理对象没关系
创建代理才是大头,调用开销很小。绝对不要循环 / 每次请求构建代理。代理对象全局单例复用。
❌误区:反射就是动态代理
JDK 代理内部用到反射,但 CGLIB FastClass 避免反射调用。
五、什么场景下动态代理会成为性能瓶颈
热点短方法:方法逻辑极轻,内存计算,无 IO,QPS 极高;代理开销占整体耗时比例高。
循环内频繁创建代理实例:Metaspace 暴涨、类加载锁竞争、GC 飙升。
高频调用,方法参数大量基础类型:JDK 代理每次生成 Object [],大量装箱。
JDK 冷启动,JIT 未完成,大量代理调用,响应延迟抖动。
大量动态代理类,类加载器无法卸载,Metaspace OOM。

六、调优实践方案
1. 使用层面
复用代理对象,全局只创建一次,禁止每次业务逻辑新建代理。
热点轻量方法,尽量减少 AOP 切面;把非核心拦截逻辑移出热点路径。
如果大量基础类型参数、超高并发热点路径:优先 CGLIB;或者考虑手写装饰器模式替代动态代理(零运行时开销)。
避免 final 类 /method,CGLIB 无法增强 final 方法。
2. JVM 参数调优
Metaspace:合理设置
-XX:MetaspaceSize,防止动态类不断扩容。类卸载:保证类加载器可以回收;不要持有代理类 Class 强引用。
JIT 预热:压测模拟真实预热流程,不要拿冷启动数据评估性能。
3. 技术选型替代
ByteBuddy:新一代字节码库,性能优于原生 CGLIB,可控动态类生成,现在很多框架改用 ByteBuddy。
编译期 AOP(AspectJ 编译织入):编译阶段直接修改字节码,没有运行期动态代理开销。性能最优;代价是编译期增强,构建流程变复杂。适合极致性能场景。
AspectJ 编译织入:没有运行时生成代理类,没有代理对象创建开销,调用开销几乎等于原生代码。
七、举一个 Spring AOP 的实际例子
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// joinPoint.proceed()
}
当使用 JDK 代理:proceed 内部底层封装为 JDK invoke;参数打包 Object []; CGLIB 模式:底层 MethodProxy,FastClass 调用。 如果被切面的方法是 DB 调用,耗时几十 ms,代理开销可以忽略不计; 如果是内存循环高频调用小方法,切面就会带来可观测损耗。
八、总结
开销分两块:创建代理(重)、调用代理(轻);最大坑点不是调用,而是重复创建代理对象。
JDK 代理损耗主要来自每次调用
Object[]装箱数组;CGLIB FastClass 规避反射,调用性能更好,但生成代理类更多。IO 密集业务系统,动态代理性能几乎不构成瓶颈;只有内存计算、超高 QPS、极短方法场景下才需要关注。
追求极致性能:复用代理对象;必要时使用 AspectJ 编译织入替代运行时动态代理。
本文原创作者:易君召,详见:https://www.yijunzhao.cn/authors/yijunzhao,转载请注明出处。
原文链接
欢迎访问 小易撩挨踢