2023年后端常见面试题
编辑为什么MYSQL使用B+树作为索引
- B+树非叶子节点不存储数据,所以每一层能够存储的索引数量会增加,同时意味着B+树在层高相同的情况下存储的数据量要比B树要多,使得磁盘IO次数更少;
- MYSQL中,范围查询是一个比较常用的操作,而B+树的所有存储在叶子节点的数据使用了双向链表来关联,所以在查询的时候只需查两个节点进行遍历即可,而B树需要获取所有节点,所以B+树在范围查询上效率更高。
- 在数据检索方面,由于所有的数据都存储在叶子节点,所以B+树的IO次数会更加稳定;
- 因为叶子节点存储所有数据,所以B+树的全局扫描能力更强一些,因为它只需要扫描叶子节点,但是B树需要遍历整个树才行。
Redis相关知识
速度快
首先Redis是将数据储存在内存中的,通常情况下每秒读写次数达到千万级别。其次Redis使用ANSI C
编写,因为C语言接近操作系统,所以Redis的执行效率很高。最后Redis的处理网络请求部分采用的是单线程,如果想充分利用CPU资源的话,可以多开几个Redis实例来达到目的,为什么单线程还是速度快的原因呢?我们知道Redis的读写都是基于内存的,读写速度都是非常快的,不会出现需要等待很长时间,所以瓶颈并不会出现在请求读写上,所以没必要使用多线程来利用CPU,如果使用多线程的话(线程数>CPU数情况下),多线程的创建、销毁、线程切换、线程竞争等开销所需要的时间会比执行读写所损耗的时间还多,那就南辕北辙了,当然这是在数据量小的时候才会这样,如果数据量到达一定量级了,那肯定是多线程比单线程快(线程数<=CPU数情况下)。
数据类型
- 五大基本数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)
- 新增基本数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)
String:缓存对象(缓存JSON对象)/缓存session信息、常规计数(统计点击量)、分布式锁(set nx)
HASH:缓存对象、购物车
SET:共同关注、抽奖去重
ZSET: 排行榜、姓名、电话排序
BITMAP:签到统计(位图1-签到0-未签到)、用户状态、连续签到总数
HYPERLOGLOG: 百万级UV统计,误差率在0.81%,只需要12K的内存空间
GEO:使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
STEAM: 消息队列
持久化
Redis可以通过RDB
和AOF
两种方式将数据持久化到磁盘上,其中这两种方式的区别如下:
- RDB:是在指定的时间间隔内将内存中的数据通过异步生成数据快照并且保存到磁盘中。
- AOF:相对于
RDB
方式,AOF
方式的持久化更细粒度,把每次数据变化(写、删除操作)都记录AOF文件中,其中AOF又可以配置为always
即实时将记录写到AOF文件中,everysec
每隔一秒将记录写到AOF文件中,no
由系统决定何时将记录写到AOF文件中。
AQS的理解
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.
从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和 共享锁。
- 排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。
- 共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。
lock 和 synchronized 区别
-
从功能角度来看,Lock 和 Synchronized 都是 Java 中用来解决线程安全问题的工具。
-
从特性来看:
-
Synchronized 是 Java 中的同步关键字,Lock 是 J.U.C 包中提供的接口,这
个接口有很多实现类,其中就包括 ReentrantLock 重入锁. -
Synchronized 可以通过两种方式来控制锁的粒度(代码块和方法声明),其中当我们使用代码块加锁时,可以通过 Synchronized 加锁对象的声明周期来控制锁的作用范围,比如锁对象是静态对象或者类对象,那么这个锁就是全局锁。如果锁对象是普通实例对象,那这个锁的范围取决于这个实例的声明周期。
-
Lock 比 Synchronized 的灵活性更高,Lock 可以自主决定什么时候加锁,什么时候释放锁,只需要调用 lock()和 unlock()这两个方法就行,同时 Lock 还提供了非阻塞的竞争锁方法 tryLock()方法,这个方法通过返回 true/false 来告诉当前线程是否已经有其他线程正在使用锁。
Synchronized 由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外,Synchronized 锁的释放是被动的,就是当Synchronized 同步代码块执行完以后或者代码出现异常时才会释放。
- Lock 提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果
已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。
而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。
Synchronized 只提供了一种非公平锁的实现。
SpringBoot启动时会加载什么?
创建Spring容器(AnnotationConfigWebApplicationContext)-> 启动TomCat容器->设置Servlate Dispatcher处理器(把Spring容器放进去,使用SpringMvc框架响应前端请求)。
如何切换tomcat/jetty?
最简单的方式是直接在pom.xml依赖文件里面去掉tomcat starter,然后引入jetty依赖。
Redis为什么满足分布式锁?
1.指令执行时单线程的,指令执行期间,不会有其他指令执行,redis数据都是放在内存里面,单线程速度执行快。
2.原子性,获取锁和加锁都是原子性的才能保证线程安全
Redisson客户端基于Redis的分布式锁又做了那些改进?
1.可重入,同一个线程可以加入多次 synchronized Reentrantlock都是可重入的。
可重入需要满足什么条件?-----> Hash结构
1.KEY 互斥条件 KEY
2.锁的信息里面要保存线程信息
3.重入次数
时间轮+递归去实现的锁续期 AP
联锁
为什么不用数据库锁的方案设计分布式锁?
乐观锁与悲观锁
「悲观锁的实现依赖于数据库自身的锁机制实现」。若是要测试数据库的悲观的分布式锁,可以执行下面的sql:select … where … for update
(排他锁),注意:where 后面的查询条件要走索引,若是没有走索引,会使用全表扫描,锁全表。排它锁是基于InnoDB存储引擎的,在执行操作的时候,在sql中加入for update
,可以给数据行加上排它锁。在代码的代码的层面上使用connection.commit();
,便可以释放锁,但是数据库复杂的加锁和解锁、事务等一系列消耗性能的操作,终归是无法抗高并发。
数据库乐观锁的方式实现分布式锁是基于「版本号控制」的方式实现,类似于**「CAS的思想」**,它认为操作的过程并不会存在并发的情况,只有在update version
的时候才会去比较。具体实现可以创建一张表锁,然后添加唯一索引,获取锁的时候insert锁信息和线程信息(也可以是其他的),释放锁的时候可以直接删除。
乐观锁实现方式还是存在很多问题的,一个是**「并发性能问题」,再者「不可重入」以及「没有自动失效的功能」、「非公平锁」**,只要当前的库表中已经存在该信息,执行插入就会失败。
但其实,对于上面的问题基于数据库也可以解决,比如:
-
不可重复,可以**「增加字段保存当前线程的信息以及可重复的次数」**,只要是再判断是当前线程,可重复的次数就会+1,每次执行释放锁就会-1,直到为0。
-
「没有失效的功能,可以增加一个字段存储最后的失效时间」,根据这个字段判断当前时间是否大于存储的失效时间,若是大于则表明,该方法的锁已经可以被释放。
-
「非公平锁可以增加一个中间表的形式,作为一个排队队列」,竞争的线程都会按照时间存储于这个中间表,当要某个线程尝试获取某个方法的锁的时候,检查中间表中是否已经存在等待的队列。每次都只要获取中间表中最小的时间的锁,也实现公平的排队等候的效果。
使用数据库分布式锁存在的问题:
- 高并发的时候如果使用数据库锁,会有很长的锁等待队列,数据库连接也被占;虽然锁等待超时会抛异常,放弃等待,等待时间也很难控制。
- 使用乐观锁会带来一系列更加复杂的问题,不仅增加了代码的开发工作量,并且犯错的风险会变得更高。
MYSQL数据库有哪几种日志,分别的作用是啥?
-
bin log 记录了对MySQL数据库执行更改的所有的写操作,包括所有对数据库的数据、表结构、索引等等变更的操作。主要应用场景分别是 主从复制 和 数据恢复。
-
redo log 是属于引擎层(innodb)的日志,称为重做日志 ,当MySQL服务器意外崩溃或者宕机后,保证已经提交的事务持久化到磁盘中(持久性)。它能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做,达到数据的持久性,一旦事务成功提交后,不会因为异常、宕机而造成数据错误或丢失。
-
undo log 是也属于引擎层(innodb)的日志,从上面的redo log介绍中我们就已经知道了,redo log 和undo log的核心是为了保证innodb事务机制中的持久性和原子性,事务提交成功由redo log保证数据持久性,而事务可以进行回滚从而保证事务操作原子性则是通过undo log 来保证的。主要应用场景:
- 事务回滚 :前面提到过,后台线程会不定时的去刷新buffer pool中的数据到磁盘,但是如果该事务执行期间出现各种错误(宕机)或者执行rollback语句,那么前面刷进去的操作都是需要回滚的,保证原子性,undo log就是提供事务回滚的。
- MVCC:当读取的某一行被其他事务锁定时,可以从undo log中分析出该行记录以前的数据版本是怎样的,从而让用户能够读取到当前事务操作之前的数据——快照读。
JVM内存结构
程序计数器(线程私有)
占用内存较小,线程私有。它是唯一没有OutOfMemoryError异常的区域。
程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。
Java虚拟机栈(线程私有)
虚拟机栈线程私有,生命周期与线程相同。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
本地方法栈
JVM 调用本地方法(Native)接口时使用,也是线程私有的,这时候使用的空间就是本地方法栈。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。也就是说本地方法也会创建栈帧,押栈和出栈。
堆
堆内存最大,堆是被线程共享,堆的目的就是存放对象。几乎所有的对象实例都在此分配。当然,随着优化技术的更新,某些数据也会被放在栈上等。
枪打出头鸟,树大招风。因为堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象),因此也称作“GC堆”(Garbage Collected Heap)。
方法区
方法区也是所有线程所共享的。它用于存储已经被JVM加载的类信息(类型信息:差不多是C的对象元信息)、常量(存堆中的地址)、静态变量、即时编译器(JIT)编译后的代码等数据信息。且设有保护程序,当一个线程正在访问的时候,另一个线程不能同时加载一个类,需要延迟等待。同时,方法区中的大小是可以改变的,运行时也可以扩展,对象也可进行垃圾回收,不过条件比较苛刻,需要没有任何引用才会进行回收。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。方法区又被称为“永久代”(JDK8之前是永久代,之后叫元空间),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已 Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
控制参数
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。
没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。 老年代空间大小=堆空间大小-年轻代大空间大小。
总结:
JAVA内存结构:
线程私有:程序计数器(记录执行的指令地址,通过此确认下一次命令要执行的地址)、本地方法栈(调用本地方法【Native】时使用)、虚拟机栈(JAVA方法执行的内存模型)
线程共享:堆(对象实例的存放地)方法区(存放类模板,类信息、常量、静态变量、即时编译器编译后的代码等数据。)
SpringBoot的理解
Springboot是什么
它并不是一个新的框架,是在spring的基础上整合了许多框架,简化了spring的很多配置,让程序能过快速,独立的运行起来。
Springboot的优点
(1)Springboot 内置有servlet容器,无需打成war包
(2)Springboot 可以直接通过主程序中的main方法运行,无需再部署到tomcat中
(3)简化spring的配置,在springboot中没有xml等相关配置
(4)起步依赖,可以在创建项目的时候直接选择需要的依赖程序加入项目中
(5)自动配置,版本控制。如需添加依赖直接在pom.xml中添加,且添加的依赖程序无需担心jar包直接版本不一致,springboot会自动匹配相应的jar包版本下载下来
(6)约定大于配置,springboot默认的配置文件是在resource下,且配置文件是以applicable-***开头的,编译的类也会在默认的targetment下
Springboot的缺点
(1)springboot也是一个微服务架构,但是它里面并没有服务的注册和服务的发现方
(2)如想将传统spring架构的项目改成springboot项目,难度比较大
SpringBoot自动装配流程和原理
线程池的5中状态
Running
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
ShutDown
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
Stop
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
Tidying
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
Terminated
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
Tomcat中为什么要使用自定义类加载器?
一个Tomcat中可以部署多个应用,而每个应用中都存在很多类,并且各个应用中的类是独立的,全类名是可以相同的,比如一个订单系统中可能存在com.zhouyu.User类,一个库存系统中,一个Tomcat,不管内部部署了多少应用,Tomcat启动之后就可能也存在com.zhouyu.User类,是一个Java进程,也就是一个JVM,所以如果Tomcat中只存在一个类加载器,比如默认的AppClassLoader,那么就只能加载一个com.zhouyu.User类,这是有问题的,而在Tomcat中会为部署的每个应用都生成一个类加载器实例,名字叫做WebAppClassLoader,这样Tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突。另外Tomcat还利用自定义加载器实现了热加载功能。
Sychronized的锁升级过程是怎样的?
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入;
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程;
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞;
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
MYSQL的MVCC机制原理
使用 InnoDB
存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
trx_id
,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在trx_id
隐藏列里;roll_pointer
,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
如上图所示,针对id=1
的这条数据,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer
属性连接成一个链表,我们把这个链表称之为版本链,根据版本链就可以找到这条数据历史的版本。
简单的例子:
可重复读REPEATABLE READ
隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView
,之后的查询就不会重复生成了。
整体流程:
- 在执行
select
语句时会先生成一个ReadView
,ReadView的trx_ids
列表的内容就是[10, 20]
,min_trx_id
为10,max_trx_id
为21,creator_trx_id
为0。 - 然后从版本链中挑选可见的记录,从图中看出,最新版本的列name的内容是
'王五'
,该版本的trx_id值为10,在trx_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 下一个版本的列
name
的内容是'李四'
,该版本的trx_id
值也为10,也在trx_ids
列表内,所以也不符合要求,继续跳到下一个版本。 - 下一个版本的列
name
的内容是'张三
',该版本的trx_id
值为8,小于ReadView
中的min_trx_id
值10,说明已经提交了,那么最终返回'张三'
。
AbstractQueuedSynchronized(AQS) 为什么采用双向链表
-
双向链表提供了双向指针,可以在任何一个节点方便向前或向后进行遍历,这种对于有反向遍历需求的场景来说非常有用。
-
双向链表可以在任意节点位置实现数据的插入和删除,并且这些操作的时间复杂度都是 O(1),不受链表长度的影响。这对于需要频繁对链表进行增删操作的场景非常有用。
-
存储在双向链表中的线程,有可能这个线程出现异常不再需要竞争锁,所以需要把这些异常节点从链表中删除,而删除操作需要找到这个节点的前驱结点,如果不采用双向链表,就必须要从头节点开始遍历,时间复杂度就变成了 O(n) 。
-
新加入到链表中的线程,在进入到阻塞状态之前,需要判断前驱节点的状态,只有前驱节点是 Sign 状态的时候才会让当前线程阻塞,所以这里也会涉及到前驱节点的查找,采用双向链表能够更好的提升查找效率。
-
线程在加入到链表中后,会通过自旋的方式去尝试竞争锁来提升性能,在自旋竞争锁的时候为了保证锁竞争的公平性,需要先判断当前线程所在节点的前驱节点是否是头节点。这个判断也需要获取当前节点的前驱节点,同样采用双向链表能提高查找效率 。
总而言之,采用单向链表不支持双向遍历,而 AQS 中存在很多需要双向遍历的场景来提升线程阻塞和唤醒的效率
谈谈ConcurrentHashMap的扩容机制
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的;
- 每个Segment相对于一个小型的HashMap;
- 每个segment内部会进行扩容,和HashMap的扩容逻辑类似;
- 先生成新的数组,然后转移元素到新数组中;
- 扩容的判断也是每个segment内部单独判断的,判断是否超过闻值。
1.8版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现;
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容;
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurentHashMap中,然后判断是否超过值,超过了则进行扩容;
- ConcurrentHashMap是支持多个线程同时扩容的;
- 扩容之前也先生成一个新的数组;
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作。
CopyOnWriteArrayList的底层原理是怎样的
- 首先Copy0nwiteAraylist内部也是用过数组来实现的,在向Copy0nWiteAraylist添加索时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进
行 - 并且,写操作会加锁,防止出现并发写入丢失数据的问题
- 写操作结束之后会把原数组指向新数组
- CopyOnWiteArraylist允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场,但是CopyOnWhiteArraylist会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
Java中的异常体系
- Java中的所有异常都来自顶级父类Throwable。
- Throwable下有两个子类Exception和Error。
- Erro表示非常严重的措误,比java.lang.StackOverFlowError和java.lang.OutOfMemoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是模拟机、磁盘、操作系统层面出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,程序可能已经根本运行不了了。
- Exception表示异常,表示程序出现Exception时,是可以程序自己来解的,,比NulPointerException、llegalAccessExceptio等,我们可以捕获这些异常来做特处理。
- Exception的子类通常又可以分为RuntimeException和非RuntimeException两类。
- RuntimeException表示运行期异常,表示这个异常是在代码运行过程中抛出的,这些异常是非检查异常,程序中可以选择浦获处理,也可以不处理,这些异常一般是由程序逻辑措误引起的,程序应该从逻辑角度尽可能避免这类异常的发生,比如NullPointerException、IndexOutOfBoundsException等.
- 非RuntimeException表示非运行期异常,也就是我们常说的检查异常,是必须进行处理的异常,如果不处理,程序就不查异常通过,如IOException、SQLException等以及用户自定义的Exception异常。
项目如何排查JVM问题
对于还在正常运行的系统:
- 可以使用jmap来查看JVM中各个区域的使用情况;
- 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁;
- 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了;
- 通过各个命令的结果,或者jvisualvm等工具来进行分析;
- 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是此较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效;
- 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存;
对于已经发生了OOM的系统:
- 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件 -XX:+HeapDump0n0utOfMemorError -XX:HeapDumpPath=/usr/local/base;
- 我们可以利用jsisualvm等工具来分析dump文件;
- 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码;
- 然后再进行详细的分析和调试。
@Contended注解的作用和原理是什么?
解决伪共享的问题,强制把对象包装成64字节的对象,加载到缓存,防止缓存失效。
Spring Bean生命周期和执行流程
SpringBean 生命周期大致可以分为五个阶段
SpringBoot 自动加载过程中的注入过滤器(OnClassCondition、OnBeanCondition、OnWebApplicationCondition)
优先级
OnClassCondition -> OnWebApplicationCondition -> OnBeanCondition
OnClassCondition
在spring-boot-autoconfigurejar包中的spring.factories配置文件中有一个org.springframework.boot.autoconfigure.AutoConfigurationImportFilter自动化配置import过滤器,配置如下:
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
上述三个类都是AutoConfigurationImportFilter自动化配置是否匹配接口的实现类,用于过滤自动化配置类及其它类是否符合条件,该类是在org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationImportFilters方法中加载到内存之中的:
protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
}
OnWebApplicationCondition
OnWebApplicationCondition用来检测容器的类型是否符合条件(@ConditionalOnWebApplication、ConditionalOnNotWebApplication),其优先级低于OnClassCondition类。
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnWebApplicationCondition extends FilteringSpringBootCondition {
//servlet对应的应用上下文类
private static final String SERVLET_WEB_APPLICATION_CLASS = "org.springframework.web.context.support.GenericWebApplicationContext";
//reactive应用上下文类
private static final String REACTIVE_WEB_APPLICATION_CLASS = "org.springframework.web.reactive.HandlerResult";
//根据自动化配置注解元数据获取指定配置类集合注解条件的匹配结果集
@Override
protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
//新建配置类大小的结果集数组
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
for (int i = 0; i < outcomes.length; i++) {
//获取自动化配置类
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
//首先根据自动化配置类获取对应条件ConditionalOnWebApplication的配置,将其作为参数获取匹配结果
//如果匹配则返回null,否则返回匹配结果及错误日志信息对象
outcomes[i] = getOutcome(
autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnWebApplication"));
}
}
return outcomes;
}
//首先根据自动化配置类获取对应条件ConditionalOnWebApplication的配置,将其作为参数获取匹配结果
//如果匹配则返回null,否则返回匹配结果及错误日志信息对象
private ConditionOutcome getOutcome(String type) {
//如果类型为null,直接返回null(默认为已经匹配)
if (type == null) {
return null;
}
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnWebApplication.class);
//如果容器类型为servlet
if (ConditionalOnWebApplication.Type.SERVLET.name().equals(type)) {
//如果上下文类不存在,则返回不匹配信息
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
}
}
//如果容器类型是reactive
if (ConditionalOnWebApplication.Type.REACTIVE.name().equals(type)) {
//如果上下文类不存在,则返回不匹配信息
if (!ClassNameFilter.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("reactive web application classes").atAll());
}
}
//如果两个上下文类都不存在,则返回不匹配信息
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())
&& !ClassUtils.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("reactive or servlet web application classes").atAll());
}
//上下文存在,返回null
return null;
}
}
OnBeanCondition
OnBeanCondition类用来检测bean是否存在(@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnSingleCandidate)
//优先级在三个过滤其中最低
@Order(Ordered.LOWEST_PRECEDENCE)
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {
//获取bean所处的阶段
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
//根据自动化配置注解元数据判定指定配置类是否符合条件
@Override
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
//创建自动化配置类匹配结果集类
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
for (int i = 0; i < outcomes.length; i++) {
//获取自动化配置类
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
//获取自动化配置对应的条件注解元数据信息
Set<String> onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");
//获取条件匹配结果
outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class);
if (outcomes[i] == null) {
//获取配置类条件注解ConditionalOnSingleCandidate的元数据
Set<String> onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass,
"ConditionalOnSingleCandidate");
//获取条件匹配结果
outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);
}
}
}
return outcomes;
}
//根据条件注解bean的类型及注解获取匹配结果
private ConditionOutcome getOutcome(Set<String> requiredBeanTypes, Class<? extends Annotation> annotation) {
//通过过滤器及反射的方式确定bean是否存在
List<String> missing = filter(requiredBeanTypes, ClassNameFilter.MISSING, getBeanClassLoader());
//如果不存在,则返回不匹配信息
if (!missing.isEmpty()) {
ConditionMessage message = ConditionMessage.forCondition(annotation)
.didNotFind("required type", "required types").items(Style.QUOTE, missing);
return ConditionOutcome.noMatch(message);
}
//如果存在则返回null
return null;
}
- 0
- 0
-
分享