spring、垃圾回收


spring是什么

控制反转和面向切面的容器框架。IoC AOP,可以帮助用来简化开发和组件的组合使用。

谈谈对IoC的理解

引入ioc容器,实现对象之间的解耦。例如对象A依赖于对象B,如果没有ioc,就需要对象A主动创建对象B,有了ioc容器之后,就变成了ioc容器主动创建一个对象B,然后注入到A需要的地方。A获得B的过程,这个依赖过程由主动变成了被动,控制权颠倒了,所以就叫控制反转。

ioc用到的一个基本技术就是反射,这个容易具体要生成什么对象,可以在配置文件里定义,增强了可维护性。

谈谈对AOP的理解

面向对象的设计过程中,会导致大量代码的重复,不利用各个模块的重用,例如事务。AOP做得就是对一个对象或者说对象的功能进行增强,可在在执行方法前后做一些别的事情。实现方式是动态代理,具体有JDK动态代理和CGLIB动态代理,利用动态代理,可以针对某个类生成代理对象,然后在调用代理对象的方法时,可以控制这个方法的执行。

JDK动态代理和Cglib动态代理实现

动态代理、IoC、AOP

DK动态代理是基于接口的方式,换句话来说就是代理类和目标类都实现同一个接口。

CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法,这样可以保证代理类拥有目标类的同名方法。

image-20211001154904445

Spring的事务机制

spring的事务是基于数据库的事务和AOP机制的,对于使用了@Transactional注解的bean,spring会创建一个代理bean,在调用对应方法的时候,判断有没有加事务注解,如果加了就用事务管理器创建一个数据库连接,并且修改自动提交为false,执行方法,如果没有出现异常就提交事务,出现异常判断是不是指定的异常,例如rollbackfor不指定的话默认就是运行期异常,是指定的异常就会回滚,不然还是会提交事务,事务的隔离级别就是数据库的隔离级别,多个事务方法间存在传播机制,例如默认required,如果方法运行时已经在一个事务,就加入到这个事务,否则就自己创建一个新的事务。

Spring http请求过程

浏览器的http请求,被tomcat监听器监听到,然后通过过滤器,到达前置分发器dispatcherservlet,其受到http请求之后,回解析url,然后对照map获取对应的处理器,再调用具体的service业务处理方法,service又调用DAO的数据处理方法,然后返回结果。

image-20211221104303921

Spring解决循环依赖,单例setter

// 完整单例bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
// bean的早期引用,还不完整
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
// 创建bean的原始工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);

构造bean之后,提前曝光到缓存里,这时候曝光的对象是构造完成,还没注入属性和初始化。提前曝光的对象通过包装成objectFactory对象放入三级缓存(没有循环依赖时,spring创建好完成品bean之后再创建对应代理,出现循环依赖被其他对象注入时,才实时生成代理对象,没有循环依赖时,可以按着spring设计原则的步骤创建)。所以就要提前曝光又不生成代理,方法就是在对象外面包一层objectFactory,提前曝光objectFactory对象,被注入时才objectFactory.getObject实时生成代理对象,并将生成好的代理对象放到二级缓存。

提前曝光之后,注入属性,注入其他bean时,有就从缓存里读取,没有就创建对象并注入。

如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

BeanPostProcessors

image-20211221142007118

功能:在创建bean的时候,能够使用自己定义的逻辑,对bean做一些处理或者执行一些业务。

对对象进行判断,看他需不需要织入切面逻辑,如果需要,那我就根据这个对象,生成一个代理对象,然后返回这个代理对象,那么最终注入容器的,自然就是代理对象了。

image-20211221145739103

垃圾回收

如何判断对象是否死亡

  • 引用计数法,给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1,引用失效减一,为0的时候就是这个对象挂了,这个的问题就是难以解决对象之间循环引用的问题。
  • 可达性分析,通过一系列的GC roots为起点,包括虚拟机栈引用的对象,本地方法栈引用的对象,方法区中静态属性引用的对象,方法区中常量引用的对象。向下搜索,搜索走过的路径称为引用链,如果一个对象到roots之间没有任何引用链相连,那这个对象就是不可用的。

判断不可达的对象一定会被垃圾回收吗

对象在可达性分析之后如果发现没有和GC roots相连,就会被第一次标记并且进行筛选,看是否有必要执行finalize方法,没必要的话,直接回收。有必要的话,就会放置在一个队列中,gc会对队列里的对象进行第二次标记,如果finalize方法和引用链上任何一个对象建立管理,就会被移除“将要回收”的集合,否则被回收。

CMS收集器

老年代收集器,标记-清除。

  • 初始标记:stw,速度很快,gc roots直接关联的对象
  • 并发标记:同时gc和用户线程
  • 重新标记:stw,修正并发标记期间因用户程序继续运行导致标记变动的记录,stw时间比初始标记长,远远比并发标记短
  • 并发清除:同时开启用户线程和gc线程,清理判断死亡的对象

优点:并发收集、低停顿

缺点:对CPU资源敏感,并发收集阶段,占用了一部分CPU资源会使程序变慢。标记-清除算法会有大量的空间碎片产生,会给大对象的分配带来麻烦,这样就可能会老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,只能触发full gc。

G1收集器

标记整理:不对可回收对象回收,而是让存活的对象向一段移动,然后直接清理掉边界以外的内存。

标记复制:将内存分为大小相同的两块,每次使用其中一块。

image-20211221154911639

新生代+老年代。面向全堆的收集器,不需要其他新生代收集器的配合。采用标记-复制(局部)和标记-整理(整体)。

  • 初始标记:暂停用户线程,GC roots
  • 并发标记
  • 最终标记
  • 筛选回收:首先对各个redion的回收价值和成本进行排序,然后根据用户期望的gc停顿时间来制定回收计划。然后把决定回收的redion的存活对象复制到空的region中,再清理掉整个旧的region回收空间,stw

image-20211221154456814

为什么CMS不采用标记整理而是采用标记清除

CMS设计理念是减少停顿时间,最好是并发执行,如果用户线程也在执行,就不能轻易改变堆中对象的内存地址,不然会导致用户线程无法定位引用对象。而标记整理和标记复制都会移动存活的对象。

垃圾回收怎么解决跨代引用的问题

例如有些老年代对象里面有个成员变量在年轻代。hotspot在新生代使用一种卡表的方式实现记录集,卡表类似于一个数组,每个元素存放卡页的地址,老年代开辟一个个空间来存放卡页,每个卡页可存放一或多个对象,如果存在跨代引用,就标识为1,表示卡页里面有对象引用了年轻代,在年轻代gc时,会去查询数组,然后全部作为gc roots的对象。

参考

图解Spring解决循环依赖♻️

Spring面试题