前情回顾
一探
Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗 中讲到了循环依赖问题
同样说明了 Spring 只能解决 setter 方式的循环依赖,不能解决构造方法的循环依赖
重点介绍了 Spring 是如何解决 setter 方式的循环依赖,感兴趣的可以去看下
二探
既然 Spring 不能解决构造方法的循环依赖,那么它是如何甄别构造方法循环依赖的了?
所以进行了二探:再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的?
从源码的角度讲述了 Spring 是如何判定构造方法循环依赖、原型循环依赖的
感兴趣的可以去看下
大家跟源码的时候,一定要注意版本!!!
项目模拟
自认为经过了前两探,对 Spring 循环依赖的问题已了若指掌,可面对线上突如其来的循环依赖问题,楼主竟然没能一眼看出来!!!
这楼主能忍?于是楼主又跟起了 Spring 源码,看看问题到底出在哪?
SpringBoot 版本是 2.0.3.RELEASE
线上服务采用 k8s 部署,本地环境未采用 k8s 部署
本地启动从未出现循环依赖问题,线上环境也只是偶发的 pod 启动失败(提示信息直指循环依赖)
问题偶发,而非必现,很是头疼,但问题还是得解决,从提示信息着手呗
根据错误提示信息,楼主模拟出了一个简化的工程,方便我们进行问题排查
图片
非常简单,完整地址:spring-other-circular-reference
我们来看下类图
图片
MyListener 、 MyService 、 MyManager 很常规,特殊的是 MyConfig 和 MySender
图片
图片
问题复现
如果按上述工程结构,本地很难复现问题 ,反正楼主是没复现出来
我们稍做调整,将 MySender 前置,如下
图片
启动失败,错误信息如下:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myConfig': Unsatisfied dependency expressed through field 'myListener'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myListener': Unsatisfied dependency expressed through field 'myService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myServiceImpl': Unsatisfied dependency expressed through field 'myManager'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myManager': Unsatisfied dependency expressed through field 'mySender'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'mySender': Requested bean is currently in creation: Is there an unresolvable circular reference?
此刻的 Is there an unresolvable circular reference? 让楼主感到了陌生
我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!
问题分析
我们从以下几个方面来分析
BeanDefinition 扫描
目前 XML 方式的 Bean 定义越来越少,除了一些遗留的老项目,基本看不到 XML 方式的 Bean 定义了
所以我们只关注注解方式的 Bean 定义的扫描
文件夹的扫描顺序与文件夹名字的升序一致,文件的顺序与文件名的升序一致,如下所示
图片
有兴趣的可以去跟下 ConfigurationClassParser 类中 doProcessConfigurationClass 方法;楼主做了下简单的总结
图片
@ComponentScan 的处理早于 @Bean
BeanDefinition 扫描过程中,会按扫描顺序会往 DefaultListableBeanFactory 的 beanDefinitionMap 中添加 BeanDefinition ,往 beanDefinitionNames 添加 BeanName
我们来跟下源码,看是不是如上所说
图片
先被扫描的 BeanDefinition 的 BeanName 会被先添加到 beanDefinitionNames
BeanDefinition 覆盖
MyConfig 中通过 @Bean 定义了 MySender ,而 MySender 类上又用了 @Component 进行修饰
那创建 MySender 实例的时候到底调用的哪个构造方法?(有参还是无参?)
关于 Spring Boot 中创建对象的疑虑 → @Bean 与 @Component 同时作用同一个类,会怎么样?从源码的角度分析了这个问题
结论是:SpringBoot 2.0.3.RELEASE 中, @Configuration + @Bean 修饰的 BeanDefinition 会覆盖掉 @Component 修饰的 BeanDefinition
也就说 MySender 类上的 @Component 其实没用,加不加效果是一样的,这里说的 没用、效果 仅仅指的是 MySender 的 BeanDefinition
Bean 实例化顺序
BeanDefinition 用来构建实例,那么 MySender 上的 @Component 就有作用了,它决定了 MySender 的实例化顺序
是先于 MyConfig 、 MyListener 、 MyServiceImpl 、 MyManager 实例化的
我们来看下 Bean 的实例化顺序
图片
理论上来讲,先被扫描的 Bean 会先被实例化;Bean 实例化的过程中会填充属性,可能会导致后被扫描的 Bean 提前被实例化
如果 Bean 之间没有依赖,那么会严格按照 Bean 的扫描顺序实例化
再看问题
我们再回到前面的问题
图片
这种情况下,我们分析下 Is there an unresolvable circular reference? 是如何产生的
相较于 MyConfig 、 MyListener 、 MyManager 、 MyServiceImpl , MySender 是最先被扫描到的,所以它最先被实例化
因为 MyConfig 中通过 @Bean 修饰了 MySender 的 BeanDefinition
图片
会覆盖掉 MySender 自身的无参 BeanDefinition
所以会通过 MySender 的有参构造方法来创建 MySender 实例
因为有参构造方法依赖 myListener ,所以去 Spring 容器中找 MyListener 实例,没有找到则创建,然后填充 MyListener 实例的属性
以此类推,实例的创建过程如下所示:
图片
Is there an unresolvable circular reference?就此产生
相当于是变种的构造方法循环依赖
我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!
最初状态
我们还原 MySender 位置
图片
此时最先实例化的是 MyConfig ,实例化过程如下
图片
对象是都可以正常实例化、初始化的
这种情况理论上来讲是不会出现 Is there an unresolvable circular reference?
线上问题
一通分析下来,还是没能找到线上 Is there an unresolvable circular reference?的原因
很是尴尬,但是我萌生了这样的想法:是不是在 k8s 部署过程中, BeanDefinition 的扫描会有偶发的随机性?
问题修复
虽然我们没能找到线上问题的确切原因,但还是有办法去根治这个问题的
Spring 不能处理构造方法循环依赖,那我们就去规避它
删掉 MyConfig , MySender 改成
图片
或 MySender 改成
图片
还有 @PostConstruct 等,方式有很多,只要不产生构造方法循环依赖就好
总结
1、 BeanDefinition 扫描顺序
如果我们去跟源代码就会发现,以启动类为起点,扫描启动类同级目录下的所有文件夹
按文件夹名升序顺序进行扫描,会递归扫描每个文件夹
文件扫描也是按文件名升序顺序进行
从线上问题来看,对这个扫描顺序,楼主是持怀疑态度的:是 Spring 会偶发的随机扫描,还是 pod 会导致偶发的随机扫描
2、 BeanDefinition 覆盖
只要我们读了源码,了解 Spring 对各个注解的扫描顺序,就清楚它们的替换关系了
BeanDefinition 覆盖并不会影响 BeanDefinition 的扫描顺序
也就是不会改变 BeanName 在 beanDefinitionNames 中的位置,即不会影响 Bean 的示例化顺序
3、 Bean 实例化顺序
理论上来讲,先被扫描到的就先被实例化,但实例化过程中的属性填充会打乱这个顺序,会将被依赖的对象提前实例化
4、 Spring 版本
一定要结合版本来看问题
版本不同,底层实现可能会不同
我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!
6 垃圾收集算法
ZGC 采用标记 - 整理算法,算法的思想是把所有存活对象移动到堆的一侧,移动完成后回收掉边界以外的对象。如下图:
图片
6.1 JDK 16 之前
在 JDK 16 之前,ZGC 会预留(Reserve)一块儿堆内存,这个预留内存不能用于 Java 线程的内存分配。即使从 Java 线程的角度看堆内存已经满了也不能使用 Reserve,只有 GC 过程中搬移存活对象的时候才可以使用。如下图:
图片
这样做的好处是算法简单,非常适合并行收集。但这样做有几个问题:
- 因为有预留内存,能给 Java 线程分配的堆内存小于 JVM 声明的堆内存。
- Reserve 仅仅用于存放 GC 过程中搬移的对象,有点内存浪费。
- 因为 Reserve 不能给 GC 过程中搬移对象的 Java 线程使用,搬移线程可能会因为申请不到足够内存而不能完成对象搬移,这返回过来又会导致应用程序的 OOM。
6.2 JDK 16 改进
JDK 16 发布后,ZGC 支持就地搬移对象(G1 在 Full GC 的时候也是就地搬移)。这样做的好处是不用预留空闲内存了。如下图:
图片
不过就地搬移也有一定的挑战。比如:必须考虑搬移对象的顺序,否则可能会覆盖尚未移动的对象。这就需要 GC 线程之间更好的进行协作,不利于并发收集,同时也会导致搬移对象的 Java 线程需要考虑什么可以做什么不可以做。
为了获得更好的 GC 表现,JDK 16 在支持就地搬移的同时,也支持预留(Reserve)堆内存的方式,并且 ZGC 不需要真的预留空闲的堆内存。默认情况下,只要有空闲的 region,ZGC 就会使用预留堆内存的方式,如果没有空闲的 region,否则 ZGC 就会启用就地搬移。如果有了空闲的 region, ZGC 又会切换到预留堆内存的搬移方式。
7 总结
内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。
ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。
ZGC 也有一个缺点,就是浮动垃圾。因为 ZGC 没有分代概念,虽然 ZGC 的 STW 时间在 1ms 以内,但是 ZGC 的整个执行过程耗时还是挺长的。在这个过程中 Java 线程可能会创建大量的新对象,这些对象会成为浮动垃圾,只能等下次 GC 的时候进行回收。
参考
1.https://wiki.openjdk.java.net/display/zgc 2.https://openjdk.java.net/jeps/304 3.https://openjdk.java.net/jeps/376 4.https://malloc.se/blog/zgc-jdk16 5.https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg 6.https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw 7.https://www.jianshu.com/p/664e4da05b2c 8.l 9.https://www.jianshu.com/p/12544c0ad5c1
··································
你好,我是程序猿DD,10年开发老司机、阿里云MVP、腾讯云TVP、出过书、创过业、国企4年互联网6年。10年前毕业加入宇宙行,工资不高、也不算太忙,业余坚持研究技术和做自己想做的东西。4年后离开国企,加入永辉互联网板块的创业团队,从开发、到架构、到合伙人。一路过来,给我最深的感受就是一定要不断学习并关注前沿。只要你能坚持下来,多思考、少抱怨、勤动手,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你看好一个事情,一定是坚持了才能看到希望,而不是看到希望才去坚持。相信我,只要坚持下来,你一定比现在更好!如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯,帮你积累弯道超车的资本。
点击阅读原文,领取2022最新10000T学习资料