排查目标对象与代理对象相互引用滞留问题,需验证是否真存在双向持留:检查切面日志中this与target类名是否相同、目标类是否显式自注入、Arthas查看实例引用链、切面是否缓存target或误用getBean、代理创建方式是否含自注入或AopContext.currentProxy()滥用、以及作用域匹配与销毁时机是否一致。

排查业务代码中因切面设计不当引发的目标对象与代理对象相互引用滞留问题,核心在于识别“循环持有”和“生命周期错位”——这不是简单的内存泄漏表象,而是代理机制与Bean生命周期耦合失当的深层症状。关键不在于“有没有引用”,而在于“该不该在该时刻持有”。
确认是否真存在目标-代理双向持留
不能仅凭GC日志或堆快照中看到两个对象互相引用就下结论。Spring代理对象内部确实持有一个对目标对象(target)的强引用,这是正常设计;但目标对象反过来持有代理对象,属于反模式。验证方式如下:
- 在切面类中添加日志,打印被拦截方法执行时的
this.getClass().getName()和joinPoint.getTarget().getClass().getName(),若两者相同(如都是UserService$$EnhancerBySpringCGLIB$$xxx),说明目标对象已被代理类替代,此时“目标”实际就是代理本身,已构成闭环 - 检查目标类中是否存在显式字段(如
private UserService self;)并被注入了当前Bean自身——这是人为制造双向引用的典型写法 - 使用Arthas的
vmtool --action getInstances --className xxx.UserService查看运行时实例数量及引用链,观察是否有非预期的长生命周期代理实例持续驻留
检查切面通知中是否意外延长了目标对象生命周期
某些切面逻辑会把目标对象缓存、放入队列、注册为监听器或传入异步线程,导致其无法随原始Bean销毁而释放。常见高危操作:
- 在
@Around通知中将joinPoint.getTarget()存入静态Map或ConcurrentHashMap - 在
@AfterReturning中把返回结果(尤其是包含目标对象引用的DTO)提交到线程池处理 - 切面内调用
ApplicationContext.getBean()获取当前Bean,并赋值给成员变量(尤其在非单例作用域下)
审查代理对象的创建与暴露方式
Spring默认不会把代理对象注入到它所代理的Bean内部。如果出现目标对象内持有代理,大概率是以下两种主动行为所致:
-
显式自注入:在目标类中通过
@Autowired注入自身类型,且未加@Lazy,导致Spring提前暴露早期引用(early reference),形成代理→目标→代理的引用环 -
手动获取代理:使用
AopContext.currentProxy()并在目标类中保存该引用(需开启exposeProxy=true),若未严格控制其作用域和生命周期,极易滞留
验证Bean作用域与代理销毁时机是否匹配
对于@Scope("prototype")或自定义作用域的Bean,其代理对象应随Bean实例一同销毁。若发现代理滞留,需确认:
- 是否在切面中将该Bean的代理引用注册到了全局事件总线、静态监听器列表或第三方框架的回调容器中
- 是否重写了
DisposableBean.destroy()或使用@PreDestroy,但未同步清理切面中持有的相关代理引用 - 是否使用了
ObjectProvider或ObjectFactory延迟获取Bean,却在切面中缓存了其代理实例

















