后端常见问题 一、基础1.Java中的四种引用类型及其应用场景1强引用特点默认引用方式对象不会被GC回收。应用常规对象创建、缓存核心数据用户信息订单信息2软引用SoftReferenceObject ref new SoftReference(new Object());特点只有内存不足时才回收。应用内存敏感的缓存大对象缓存。内存充足时利用缓存提升性能内存紧张时自动释放资源避免oom3) 弱引用解决内存泄漏存储临时关联数据WeakReferenceObject ref new WeakReference(new Object());特点GC时必定回收应用WeakHashMap键为弱引用监听器/回调防止内存泄漏ThreadLocal4虚引用特点每次GC回收。无法通过get()获取对象需配合ReferenceQueue应用精确控制对象销毁时机代替finalize()进行资源管理直接内存管理netty、DirectByteBuffer2.HashMap、 ConcurrentHashMap的底层实现原理jdk1.8 ,数据结构数组链表红黑树。hashMapConcurrentHashMap扩容时机元素总数超过阈值数组长度*0.75写线程发现目标桶头结点是ForwardingNode表示正在迁移则协助迁移put流程如果数组为空先初始化数组默认长度为16如果数组为空先CAS初始化数组默认长度为16计算桶的位置。hashCode高16位和低16位进行异或然后与数组长度-1相与桶为空直接插入桶为空CAS插入。桶不为空遍历链表/红黑树判断key是否相同相同则覆盖不同则末尾插入桶不为空上一步CAS失败或者本身不为空synchronized锁住头结点。判断头结点是否为forwardingNode是则协助扩容。否则和左边一样的逻辑插入数据。判断桶数据是否大于8或者小于6大于8则尝试转为红黑树。小于6则树转为链表。判断是否需要扩容扩容流程单线程执行多线程并发执行1新建一个容量为原数组2倍的新数组2数据迁移hash数组长度为0数据在原位置否则在原位置原数组长度3迁移完成将新数组引用替换旧数组引用扩容期间整个map不可用效率低1新建一个容量为原数组2倍的新数组2旧数组划分为多个区间transferIndex多个线程协同分批迁移数据3当线程进行put时如果桶是ForwardingNode放下 put 的活去 CAS 抢transferIndex领取区间对区间中桶的头结点加锁协助把旧数组的数据搬到新数组。4)线程从后往前领取任务区间步长根据cpu核数和数组长度动态计算。5当所有桶的头结点都变成forwardingNode替换数组引用。桶的头结点变为forwardingNode读操作会调用find方法找到新位置3.ArrayList和LinkedList的底层差异及使用场景ArrayList底层是数组而LinkedList底层是链表ArrayList适用于随机访问适合读多写少的场景日常开发中最常用。在尾部插入时效率高不用移动大量元素。当数组容量不足时会自动触发扩容默认是原来的1.5倍。扩容时需要复制数组有一定开销。LinkedList适用于写多读少需要在头部尾部频繁插入/删除的场景。头尾插入时效率高。如果是其他节点虽然不用迁移数据但是需要定位节点从头遍历。比较适合做队列先进先出5.反射机制的原理和使用注意事项本质是在JVM运行时通过class对象动态获取类的元数据如方法、字段。绕过编译期的检查直接通过底层native方法操作对象注意事项1)性能差无法通过jvm内联优化高频调用会有明显瓶颈2破坏封装强行访问私有成员存在安全隐患3不安全编译期无法检查错误异常全部推迟到运行时才暴露业务代码中尽量不用主要用于spring框架。如果需要用就要对method等对象做缓存优化性能5.1 数据导出excel时通常使用反射进行类的字段映射如何优化性能1解决反射性能瓶颈缓存元数据。反射最大的开销在于每次获取字段或方法时都需要去遍历类的元数据。我们可以建立一个全局缓存CoucurrentHashMap,以class为key缓存该类所有字段的Field或Method对象。这样在首次加载某个类时仅触发一次反射后续几十万行数据的转换查询缓存即可性能会有质的飞跃2解决大数据量内存溢出问题使用SXSSFWorkbook或直接使用excel开源框架easyExcel。数据分批在业务层做分页查询将数据转换和写入excel的动作拆分成更小批次构建一批写入一批释放一批。避免数据在内存双重驻留6.泛型擦除机制和类型擦除带来的问题泛型只在编译阶段有效用于做类型安全检查。编译完成运行时所有泛型信息会被擦除。即编译器会将所有泛型参数如T替换为其上界Object。问题1无法使用基本数据类型泛型擦除后被替换成Object基本类型不是对象只能使用包装类这里拆箱、装箱就会带来额外的性能开销2无法进行instanceof判断或new实例化。因为运行时泛型信息不存在所以代码里写if(obj instanceof ListString )编译报错也无法new T();3) 容易引发隐蔽的运行时异常其实也是反射会带来的问题我们可以通过反射往ListString 中塞入一个Integer编译器不会报错但后续遍历这个列表就会报错二、JVM与性能调优1.JVM内存结构:堆、栈、方法区、元空间线程共享堆、元空间、方法区堆堆是JVM中最大的一块内存存放所有对象实例和数组。像包装类在-128到127之间会用到常量池常量池也在堆中但底层会直接复用缓存对象不会重复创建垃圾回收的主要区域。通常分为新生代eden、survivor、老年代。方法区存储类信息、常量、静态变量、即时编译器编译后的代码元空间jdk8以前方法区由永久代实现因为永久代内存小容易OOM。JDK8以后永久代被移除改为由元空间实现他使用本地内存大大降低OOM风险线程私有虚拟机栈、本地方法栈、程序计数器虚拟线程每个方法执行都会创建一个栈帧存放局部变量表、操作数栈、动态链接等。方法调用就入栈结束就出栈程序计数器记录当前线程执行的字节码指令地址保证线程切换后能恢复到正确位置2.栈上分配栈上分配的核心目的就是为了避免对象进入堆内存从而减少 GC 压力提升程序性能。就是对象只在方法里有使用到不作为外部被使用比如作为返参那他就会被分配到栈上3.垃圾回收算法标记清除、复制、标记整理标记-清除先标记出所有存活对象然后直接清除未标记的垃圾优点是简单缺点是会产生大量内存碎片。复制算法将内存分为两块每次只用一块存活对象直接复制到另一块并清空当前块没有内存碎片但浪费了一半内存空间。标记-整理先标记存活对象然后让所有存活对象向一端移动最后清理掉边界以外的内存解决了碎片问题但移动对象成本较高。4.GC Root绝对不会被GC回收的对象可以被作为GC Root。一个对象能从任意GC Root出发有引用链连通的那么这个对象就不可被回收。比如虚拟机栈中、本地方法栈中引用的对象还有方法区中的静态变量、常量引用的对象运行时常量池中的常量——字符串常量池的字符串同步锁持有的对象、JVM内部引用的关联对象系统类加载器、class对象。如果我们在开发中使用静态集合存大量对象却忘记清理或者使用ThreadLocal没有调用remove那么他们就会被gc root 锁住而无法被回收最终导致内存泄漏。5.类加载机制和双亲委派模型类加载机制jvm将class文件加载到内存进行解析、验证、初始化为class对象的过程。双亲委派就是JVM有3类类加载器启动类加载器、扩展类加载器、应用类加载器。当一个类加载器收到请求时先不自己去加载而是交给父类父类加载不了再自己加载。为什么防止类被重复加载防止java加载恶意类覆盖核心类保证java核心库的安全。为什么打破双亲委派过于僵化特定场景父加载器无法加载子类加载器路径下的类或者需要实现动态替换。1如JDBC驱动加载JDBC接口由启动类加载但具体驱动实现是由应用类加载器加载。父类加载器无法感知子类路径下的类。2热部署与动态加载Tomcat容器需要支持同一个应用的不同版本同时运行或者在不重启服务器的情况下替换某个类。需要使用自定义类加载器去打破实现类的隔离与热替换。如何实现使用线程上下文类加载器加载驱动自定义类加载器6.内存泄漏的排查方法和工具使用jstat -gcutil pid命令观察老年代Old Gen的使用率是否持续上升且 Full GC 后无法回落。jmap -dump:live pid命令。加上live参数可以只导出存活对象减小文件体积生成堆dump文件使用VisualVM或MAT进行分析7.JVM线上问题排查:CPU飙升、内存泄漏、死锁CPU飙升通常由死循环或频繁GC引起使用top -H定位高耗线程并结合jstack分析调用栈内存泄漏表现为老年代持续增长通过jstat确认后用jmap导出堆快照借助 MAT 分析对象引用链死锁导致线程互相等待直接运行jstack即可在输出中查看 JVM 自动检测到的死锁线程及锁依赖信息。并发编程1.synchronized和ReentrantLock的实现原理及区别synchronizedJVM 关键字底层基于对象头 Mark Word 和 Monitor 监视器通过 CAS 和自旋实现锁升级偏向锁 - 轻量级锁 - 重量级锁。ReentrantLockJDK 类底层基于 AQS 框架通过 CAS 修改 state 变量抢锁失败则进入 CLH 队列阻塞等待。核心区别锁释放前者隐式自动释放后者必须在 finally 中手动释放。公平性前者只支持非公平锁后者支持公平和非公平锁。条件变量前者只有单一等待队列wait/notify后者支持多路 Condition 精准唤醒。锁获取前者阻塞不可中断、无超时后者支持响应中断 lockInterruptibly 和超时获取 tryLock 。2.volatile关键字的作用和内存语义保可见一改全改大家立刻能看到最新值。禁重排防止编译器和CPU乱序执行代码。不保原子像 i 这种复合操作依然不安全。内存语义1内存屏障Memory Barrier写操作在写 volatile 变量前后插入屏障禁止普通写与后面的volatile重排 volatile 写后面的 volatile 读/写重排。读操作在读 volatile 变量后插入屏障禁止 volatile 读与后面的普通读写重排。2JMM 规范约束强制要求线程在读取 volatile 变量时必须从主内存读取写入时必须立刻刷新到主内存且工作内存中的缓存副本失效。3.Java线程池的核心参数和工作原理4.ThreadLocal的实现原理和内存泄漏问题5.AQS原理6.CountDownLatch、CyclicBarrier、 Semaphore的使用场景7.线程间通信的几种方式8.虚拟线程使用synchronized为什么会被钉住因为锁的归属记录在载体线程上如果此时 JVM 强行卸载虚拟线程载体线程就会变成“自由身”。如果这个载体线程随后去获取了其他锁就会导致底层的锁状态混乱甚至死锁。为了保证锁的安全性JVM 只能选择将虚拟线程死死“钉”在载体线程上导致载体线程跟着一起傻等无法释放 。而ReentrantLock 底层做了特殊处理允许虚拟线程安全卸载 。二、Spring框架生态1.Spring Bean的生命周期实例化属性赋值初始化initializingBeanbeanPostProcessorBeforeinit-method,beanPostProcessorAfter,使用bean销毁2.SpringlOc和DI的实现原理控制反转将对象创建和对象之间的依赖关系交给spring管理。创建完的对象就放在一级缓存里。创建对象时发现对象需要依赖其他对象时就会去一级缓存getBean找到后就进行set赋值或者构造函数。3.SpringAOP的实现原理JDK动态代理和CGLIB区别aop面向切面编程跟业务无关的逻辑比如日志权限事务就使用aop统一管理。aop底层就是动态代理对目标类进行增强逻辑当调用目标方法切点时根据通知类型进行增强比如前置通知后置通知环绕通知异常通知等目标类实现了接口使用jdk否则就是gclib4.Spring事务管理机制和传播特性aop实现threadLocal绑定事务管理器。传播特性就是当方法中存在多个事务时要如何处理主要有以下几个requiredrequired_new嵌套事务。required当前存在事务加入当前事务否则新建一个。内部事务回滚外部的也跟着回滚。转账给A加钱给B减钱两个方法要么全成功要么全失败。减钱方法失败了加钱方法也要回滚required_new当前存在事务将事务挂起新建一个事务。内部事务回滚外部的不会。比如下单和记录日志。无论下单失败还是成功都要记录日志。嵌套事务当前存在事务嵌套到当前事务。内部事务回滚外部的不会。比如发放积分下单失败发放积分也要回滚。发放积分失败下单不能回滚。5.BeanFactory和ApplicationContext的区别6.Spring中的设计模式应用7.Spring Boot自动配置原理8.Spring Boot启动流程原生IOC启动流程1. 加载配置资源2. 解析为BeanDefinition3. 初始化Bean工厂、执行工厂后置处理器、注册后置处理器4. 实例化单例Bean、依赖注入、生命周期全程只管BeanSpringBoot启动流程1. 初始化SpringApplication准备监听器、初始化器2. 准备运行环境、加载配置文件3. 打印Banner、创建应用上下文4. 执行自动配置大量自动配置类生效5. 刷新IOC容器这里才开始走上面原生IOC全套流程6. 启动内嵌Tomcat7. 执行收尾回调、应用就绪9.Spring Boot Starter的工作原理10.Spring Boot配置文件加载顺序和优先级先外部再内部11.如何实现Spring Boot应用的优雅停机停止接收新请求、等待正在处理的请求完成、安全释放资源如数据库连接、线程池等最后退出应用 。springBoot框架原生支持优雅停机这是最简单且官方推荐的方式添加配置server.shutdown。设置等待请求处理完毕时间当应用接收到停止信号后停止接收新的连接请求。三、数据库与存储MySQL1.MySOL索引的数据结构(B树原理)多路平衡查找树BTree 有序键 非叶只指路 叶存数据/主键 叶间链表 → 点查快、范围查更快。✅ 高度低3~4层 → 磁盘 IO 少✅ 叶子链表 → 范围查询 BETWEEN 、 高效✅ 节点填充率高 → 节省空间非叶子节点不存储数据2.聚簇索引和非聚簇索引的区别索引和数据是否在一个文件聚簇索引主键即聚簇索引叶子节点存放 整行数据表数据本身就是按主键 BTree 组织的。非聚簇索引叶子节点存 索引列值 对应主键值查非索引列时需 回表用主键再去聚簇索引查整行3.最左前缀原则和索引下推最左前缀原则是“索引能不能用”的匹配规则而索引下推是“索引用了之后如何优化”的执行机制。最左前缀原则决定了联合索引的命中范围。它要求查询条件必须从联合索引的最左边字段开始匹配一旦遇到范围查询如,,BETWEEN,LIKE或匹配中断索引就会停止向右匹配。索引下推ICP决定了回表的时机。当联合索引遇到范围查询如 、 、 LIKE 前缀% 导致最左前缀原则中断时后续列虽然无法用于“索引查找”但它们依然存在于索引树中。此时 ICP 允许引擎利用这些“后缀列”进行提前过滤从而极大减少回表次数。索引下推针对联合索引的查询优化机制。在索引层面提前过滤减少不必要的回表次数。从而显著降低磁盘 I/O提升查询性能。举例索引下推只有在联合索引“部分命中”即发生了索引中断时才会发挥作用。比如联合索引是(a, b, c)你查a1 and c2b缺失此时最左前缀原则导致索引只匹配到a而剩下的c2这个条件就会触发索引下推在索引层直接过滤。再比如name是索引查询 SELECT * FROM table WHERE name LIKE 张% AND name ! 张三;没有ICP时会在索引中扫描出所有姓张的记录然后逐条回表查出所有完整数据最后在server层过滤不等于张三的记录。有ICPname ! 张三 这个条件可以直接在索引里判断不需要回表存储引擎会在扫描索引时直接把 name 张三 的索引项剔除掉只对剩下的记录进行回表减少回表次数4.SOL优化经验和慢查询分析1EXPLAIN 分析执行计划核心手段重点关注以下核心字段type访问类型性能从优到劣依次为 const eq_ref ref range index ALL。生产环境应严禁出现 ALL全表扫描。key实际使用的索引。如果为空说明未使用任何索引。rows预估扫描的行数数值越小效率越高。Extra额外信息。若出现 Using filesort文件排序或 Using temporary临时表说明存在典型性能瓶颈。2 SQL优化实战经验1. 索引优化策略合理建索引在高频的 WHERE、JOIN、ORDER BY、GROUP BY 字段上建立索引。遵循最左前缀原则设计复合索引时将等值条件放在前面范围查询和排序字段放在后面。利用覆盖索引尽量让查询的字段全部包含在索引中避免回表操作。避免索引失效不要在索引字段上做函数运算、隐式类型转换或进行前置模糊查询如 LIKE %xxx。2. SQL 写法重构杜绝 SELECT *只查询业务必需的字段减少网络传输和内存消耗。优化大分页对于 LIMIT 100000, 20 这种深分页改用游标方式如 WHERE id last_id LIMIT 20或延迟关联大幅减少扫描行数。减少子查询与关联避免复杂的关联子查询尽量改写为 JOIN合并结果集时若允许重复使用 UNION ALL 代替 UNION。5.MySQL事务隔离级别和MVCC原理读未提交读已提交可重复读串行MVCCundolog版本链和readView快照。查询数据时创建readView然后在undolog版本链里通过对比readView的一些属性找到能够被当前事务可见的版本。读已提交和可重复读的实现区别在于生成readView的时机一个是每次读都生成一个是第一次读生成。6.数据库锁机制行锁、表锁、间隙锁行锁包括了记录锁间隙锁临键锁。对于唯一索引进行等值查询时加的就是记录锁对于非唯一索引的范围查询时加的是间隙锁。锁不存在的记录防止其他事务在范围里插入不存在的数据而产生幻读。临键锁锁记录和记录前的数据。8.主从复制原理和读写分离实现binlog实现。三个线程dumpiosql。主节点dump线程发送binlog给从节点从节点接收binlogio线程将其写入RelayLog中继日志从节点sql线程从中继日志回放sql主从延迟问题提升从节点硬件性能从节点避免慢查询占用CPU建主键索引避免从节点回放sql时全表扫描使用半同步复制Redis1.Redis的数据结构和应用场景2.Redis持久化机制:RDB和AOF对比RDB 定时快照丢数据多但恢复快AOF 实时追加数据更安全但文件大生产用混合持久化。RDB 不是固定间隔触发而是基于配置的时间窗口变更次数阈值触发如果业务写入量低可能很长时间不生成 RDB这也是它丢数据风险的根源。3.缓存穿透、缓存击穿、缓存雪崩的解决方案4.Redis内存淘汰策略5.分布式锁6.缓存数据库一致性问题四、消息队列与中间件消息队列1. Kafka、 RabbitMO、RocketMO的选型对比2.如何保证消息不丢失3.如何保证消息顺序性4.消息堆积的处理方案5.延迟消息的实现方式临时存储到延迟主题不同延迟级别放不同队列。定时任务投放4.x版本生产者发送延时消息时通过setDelayTimeLevel方法设置延时级别消息发送到Broker。Broker收到延时消息后不会将其写入真实的业务主题而是写入系统级延时主题SCHEDULE_TOPIC_XXXX每个延时级别对应该主题下的一个独立队列。Broker为每个延时级别队列启动一个独立的调度线程持续轮询队列中的消息判断消息是否到达投递时间。当消息到达投递时间后调度线程会将消息从延时主题转移到用户指定的真实业务主题此时消息对消费者可见会被正常投递。消费者监听业务主题收到消息后执行对应的延迟业务逻辑。5.x版本定时消息的核心实现基于TimerStore定时索引存储与多级时间轮调度机制。灵活可靠6.消息队列的事务消息解决“本地事务如数据库操作”与“消息发送”之间的原子性问题确保两者要么同时成功要么同时失败实现分布式事务的最终一致性。流程生产者发送事务消息broker接收并存为半消息目标主题和队列存属性因此消费者看不到这条消息。broker接收消息后生产者执行本地事务将执行结果发送给brokerbroker姐收到后做相应处理记录消息偏移量到op队列如果是提交就投放到目标主题。回查时间间隔transactionTimeout事务超时时间。Broker 在此时间之后才会开始对该事务消息进行首次回查4.x版本通常默认是60秒。transactionCheckInterval回查间隔。默认值有说是60秒或30秒具体取决于版本Broker配置文件中通常以毫秒为单位RocketMQ 默认最多回查 15次如果本地事务一直返回 UNKNOW 状态达到上限后 Broker 会停止回查并默认回滚丢弃该消息 。为了防止这种极端情况导致数据不一致我在项目中做了以下处理”将事务状态落库回查时直接读库判断尽量在短时间内给出明确的 Commit 或 Rollback避免长时间处于中间状态 。监控告警通过采集 Broker 的错误日志监控回查失败率。如果同一消息连续回查失败超过 3 次就会触发告警提前介入处理 。RocketMQ 事务消息和本地消息表方案怎么选事务消息实现复杂度低不需要额外建表耦合度低但强依赖 MQ 的支持本地消息表需要自己建表和定时任务业务侵入性强但适用于任何 MQ一致性由业务层面保证