Blogs

JVM垃圾回收器

**Java分配对象的过程以及新生代和老年代划分的目的

创建一个新的对象实例时,jvm首先会在堆内存分配内存空间,大部分情况下,新对象都会分配到新生代的Eden区,新生代有三个区,一个Eden区和两个survivor区,当Eden区满了以后会进行Minor GC(新生代GC,是指新生代的垃圾收集,一般Eden区满了就执行,非常频繁,回收速度快),在GC的过程中存活的对象会在两个survivor区中进行转移和交换,经过多次GC任然存活的对象会被放入老年代,老年代主要用于存储长期存活的对象或者是大对象。

新生代中如果有比较大的对象比如数组list那些,会直接放入老年代里面。

划分新生代和老年代的主要目的有两个:

  • 新生代采用的是一种简单高效的复制算法进行垃圾回收,可以快速完成垃圾回收减少暂停时间,因为大部分对象都是暂时存在的,所有这种策略能够有效处理大量短暂对象的分配和回收
  • 可以针对不同的对象采取不同的回收策略,新生代频繁回收,老年代较少回收,可以减少full GC(全面垃圾收集,清理新生代与老年代以及方法区,full GC通常比Minor GC慢很多,因为full GC涉及到整个栈的回收,并且在GC期间,应用程序的所有线程都会暂停)的频率,提升系统的整体性能

**标记清除、复制和标记压缩三种垃圾回收算法的基本原理

标记清除算法: 遍历所有可达对象标记为存活的状态,然后遍历堆内存,把没有标记的对象全部视为垃圾进行清理

  • 优点 简单,不需要额外的内存空间
  • 缺点 会产生大量的内存碎片,而且效率很低

复制算法: 把内存分为两个相等的区域,每次只使用其中一个区域,当这个区域满了以后,把存活的对象复制到另一个区域中并且清除原区域的所有对象

  • 优点 每次垃圾回收后内存都是连续的,不存在内存碎片
  • 缺点 需要额外的占用内存空间,并且对象频繁复制导致效率问题

标记压缩算法: 先标记所有可达对象,然后把存活的对象向一端移动,然后直接清理边界外的内存区域,从而消除碎片 ![[Pasted image 20250513172753.png]]

  • 优点 解决标记清除算法所造成的内存碎片问题,相对复制算法减少了内存空间占用
  • 缺点 复杂度高,而且执行效率相对比较低,特别是压缩阶段需要移动对象,可能会引起程序暂停的时间较长

serial、Parallel、CMS和G1垃圾回收器的主要特点

  • serial GC是串行垃圾回收器,比较适用于单核处理器或者对响应时间要求不高的场景

  • Parallel基于多线程并行垃圾回收器,适合高吞吐量的服务器应用或者CPU核心数较多的服务器坏境

  • CMS也是并且垃圾回收,但是他会把垃圾回收分为四个阶段,尽可能减少了STW(系统在执行特定操作时需暂停所有应用程序线程)的时间,比较适用于高交互性的应用,比如web服务器,以及对停顿时间有严格要求但是对吞吐量比较宽松的场景

  • G1把堆内存划分了多个大小相等的区域,每个区域都可以独立作为新生代和老年代的一部分,通过并行和并发实现垃圾回收,从而减少停顿时间,另外还能根据目标停顿时间来动态调整垃圾回收策略,来满足不同需求,适合低延迟和可预测的垃圾回收停顿时间的应用,比如大规模分布式系统、在线交易系统

SpringApplication.run 执行后的四个阶段

四阶段分别为:服务构建、环境准备、容器创建和填充容器

服务构建

  • 首先把传入的资源加载器、主方法类记录到内存中,然后逐一判断对应的服务类是否存在来确定web服务的类型
    • 默认是基于servlet的web服务,如tomcat,还有响应式非阻塞服务reactive,如spring-webflux,还有什么都不是的none
  • 确定完选择哪个web服务后就是加载初始化类了,会去读取META-INF/spring.factories文件中的注册初始化、上下文初始化和监听器这三个配置
  • 最后是通过运行栈stackTrace判断main方法所在类

环境准备

  • 先new一个启动上下文 bootstrapContext,然后调用启动注册初始化器中的初始化方法 initialize,但由于没有没默认的初始化器,所以也没初始化什么(这个可以靠手动添加)
  • java.awt.headless 设置为 true,表示缺少显示器、键盘等输出设备也能正常启动
  • 然后启动运行监听器,同时发布启动事件,获取并加载springboot工程配置文件中监听器,就可以做到通过监听事件在启动的流程中加入自定义逻辑
  • 接下来就是组装启动参数,例如根据不同的web服务构造不同的环境(默认是servlet)、坏境变量、jvm系统属性等,把这些信息加载到一个内存集合中,后续调用就无需重新加载了

容器创建

  • 根据服务类型创建容器(默认servlet)注解配置的servlet-web服务容器
    • 存放和生产bean实例的Bean工厂
    • 用来解析 @Component@ComponentScan 等注解的配置类后处理器
    • 用来解析 @AutoWired@Value等注解的自动注解bean处理器
  • 对容器中的部分属性进行初始化

填充容器

  • 生产自身提供或者自定义的所有Bean对象,放入容器创建步骤中创建好的容器中,这个过程也叫做自动装配
  • 构造启动web服务器
  • 回调自定义实现的 Runner 接口,来处理执行后定制化的需求

SpringBoot启动流程

首先需要一个加了 @SpringBootApplication 注解的启动类,这个注解本质上就是由 @EnableAutoConfiguration@SpringBootConfiguration@ComponentScanner 连起来构成。

  • @EnableAutoConfiguration 的作用是在启动时自动加载一个类,这个类会将所有符合条件的 @Configuration 配置都进行加载,如果启动类中不需要添加配置内容,也不需要扫描路径,可以将 @SpringBootApplication 换成 @EnableAutoConfiguration

  • @SpringBootConfiguration 等同于 @Configuration,就是将这个类标记为配置类,会被加载到容器中

  • @ComponentScanner 就是自动扫描并加载所有符合条件的 Bean

注解完成后,运行的起点就是 SpringApplication.run(类名.class, args),在 run 开始执行后会经历四个阶段:服务构建、环境准备、容器创建和填充容器

MySQL事物的原理是什么

MySQL满足ACID的特性,所以MySQL事物的原理就是innodb是如何去实现ACID的特性。

首先A就是原子性,就是要保证DML数据库操作语言要么都成功,要么都失败,都成功好理解,如果都失败就意味着要把原本执行的操作都回滚,所以innodb里面设计了一个undo log表,在事物执行的过程中把执行数据的快照保存在undo log表里,例如执行一个insert语句,在undo log表里就存储一个delete语句,一旦出现错误就直接读取undo log执行反向操作就行了。

其次就是C一致性,表示数据的约束没有得到破坏,这个更多是依靠业务层的保障,数据库里面也提供了像主键约束,唯一约束,字段长度约束等。

I是隔离性,多个并行事物对同一个数据进行操作如何去避免多个事物的干扰导致数据混乱。innodb里面实现了SQL92的标注,提供了四个隔离级别的实现,分别是未提交读、已提交读、可重复读以及串行化。innodb默认实现的是可重复读,并且使用了MVCC解决了脏读和不可重复读的问题,然后使用了行锁或者表锁的方式来解决幻读的问题。

D是持久性,也就是说事物提交后的数据一定是永久化保留,不能因为数据库宕机或者其他原因导致数据变更的失效。理论上说事物提交后直接放在磁盘保存就好了,但是因为随机磁盘IO的效率确实很低,所以innodb设计了Buffer pool缓冲区来进行优化,数据更新的时候先更新缓冲区,然后在合适的时间持久化到磁盘里。但是在这个过程中可能会因为数据库宕机导致数据丢失,因此innodb引入了redo log文件,这个文件存储了数据库变更后的值,我们通过事物进行数据更改的时候,除了修改内存缓冲区里的数据以外,还会被本次修改的值追加到redo log里面,当事物提交的时候直接把redo log里面的日志刷新到磁盘里面进行持久化,一旦数据库宕机在MySQL重启以后可以直接用redo log里面保存的重写日志读取再执行一遍。

因此认为,MySQL事物的原理就是innodb如何实现ACID的特性,用到了MVCC、行锁、表锁、缓冲区、redo log和undo log来实现。

Redis常见的几个问题

缓存雪崩

当大量缓存在同一时间失效或者过期,亦或者Redis故障宕机时,如果此时有大量用户数据访问,Redis 无法处理,于是全部请求都会直接访问数据库,导致数据库压力剧增,严重会导致数据库宕机,从而出现更严重的问题

常见解决方法:

  • 给各数据设置不相同的过期时间,尽量让他们不要在同一时间过期
  • 设置互斥锁,当发现需要查找的数据在Redis中找不到,就加上一个互斥锁,保证一个时间只有规定的次数查询数据库来构建缓存
  • 如果是宕机,那我们可以用熔断来解决,或者是拒绝服务

缓存击穿

缓存击穿跟缓存雪崩很相似,可以认为缓存击穿是缓存雪崩其中之一

如果缓存中某个热点数据过期了,此时大量的数据请求访问了该数据,就无法从缓存中读取,直接访问数据库,数据库很容易被高并发的请求冲垮,这就是缓存击穿问题。

常见解决方法:

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

缓存穿透

当用户访问的数据不存在,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,那么此时大量的请求都落在数据库中,导致数据库压力骤增

常见解决方法

  • 限制非法请求,进行参数校验,对于不合法的参数请求直接抛出异常返回给客户端。
  • 缓存空值或者默认值,当发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,使其不会继续查询数据库
  • 使用布隆过滤器,过滤器说数据不存在,那么数据库中一定不会有这个数据。如果说数据存在,并不一定证明数据库中存在这个数据,有误判的几率,只不过几率非常小。(底层是hash,有极小概率误判)