版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
Java后端开发工程师高频面试题
【精选近三年60道高频面试题】
【题目来源:学员面试分享复盘及网络真题整理】
【注:每道题含高分回答示例+避坑指南】
1.请做一个自我介绍(基本必考|重点准备)
2.聊聊HashMap的底层实现,JDK1.7和1.8有什么区别?为什么1.8要引入红黑树?(极高
频|基本必考)
3.ConcurrentHashMap是如何保证线程安全的?CAS+synchronized是怎么配合的?(极
高频|重点准备)
4.请手写一个单例模式(DCL双重检查锁),并解释为什么需要volatile关键字?(基本必
考|背诵即可)
5.线程池的7个核心参数分别是什么?生产环境如何合理配置线程池大小?(极高频|考察
实操)
6.什么是AQS(AbstractQueuedSynchronizer)?ReentrantLock的加锁和释放锁流程是怎
样的?(常问|需深度思考)
7.讲一下JVM的内存模型(JMM),堆和栈的区别是什么?(基本必考|背诵即可)
8.详细介绍一下CMS和G1垃圾收集器的区别,ZGC了解吗?(常问|重点准备)
9.生产环境出现OOM(内存溢出)或CPU飙升到100%,你的排查思路和步骤是什么?
(极高频|考察实操)
10.类加载机制中的“双亲委派模型”是什么?如何破坏双亲委派?(常问|需深度思考)
11.MySQL索引的数据结构为什么选择B+树而不是B树或Hash?(基本必考|重点准备)
12.谈谈MySQL事务的隔离级别,以及MVCC(多版本并发控制)的实现原理。(极高频|需
深度思考)
13.这是一条执行很慢的SQL,你会如何进行分析和优化?(Explain关键字解读)(极高频|
考察实操)
14.MySQL的锁机制了解吗?什么是间隙锁(GapLock),如何避免死锁?(常问|需深度
思考)
15.Redis有哪些数据类型?在你的项目中具体用在什么场景?(基本必考|网友分享)
16.Redis的持久化机制RDB和AOF有什么区别?生产环境怎么选?(常问|背诵即可)
17.什么是缓存穿透、缓存击穿和缓存雪崩?分别有什么解决方案?(极高频|重点准备)
18.如何实现分布式锁?基于Redis(Redisson)和Zookeeper实现的区别是什么?(重点准
备|考察实操)
19.SpringBean的生命周期是怎样的?BeanPostProcessor在其中起什么作用?(常问|背诵
即可)
20.Spring是如何解决循环依赖问题的?三级缓存的作用分别是什么?(常问|需深度思考)
21.SpringBoot的自动装配原理是什么?@SpringBootApplication注解背后做了什么?(基
本必考|重点准备)
22.动态代理JDKProxy和CGLIB有什么区别?SpringAOP默认使用哪种?(常问|背诵即
可)
23.消息队列(MQ)在使用中如何保证消息不丢失?(以RocketMQ或Kafka为例)(重点
准备|考察实操)
24.如何保证消息消费的顺序性?如果出现了消息积压怎么处理?(极高频|考察抗压)
25.了解分布式事务吗?讲讲Seata的AT模式或TCC模式的原理。(常问|需深度思考)
26.微服务架构中,服务熔断和降级有什么区别?Sentinel或Hystrix的原理了解吗?(常问|网
友分享)
27.什么是CAP定理?BASE理论又是什么?注册中心Eureka和Nacos在一致性上有什么区
别?(基本必考|需深度思考)
28.Netty的线程模型(Reactor模式)是怎样的?为什么它性能高?(常问|重点准备)
29.TCP的三次握手和四次挥手流程是怎样的?为什么握手是三次而挥手是四次?(基本必
考|背诵即可)
30.HTTPS的加密流程是怎样的?对称加密和非对称加密是如何配合的?(常问|需深度思
考)
31.Session、Cookie和Token(JWT)的区别是什么?分布式Session如何实现?(基本必
考|网友分享)
32.什么是布隆过滤器(BloomFilter)?它的实现原理和误判率是怎么回事?(常问|需深度
思考)
33.常用设计模式有哪些?请结合你的项目代码,举例说明你是怎么使用的(如策略模式、模
板方法)。(极高频|考察实操)
34.Linux常用命令有哪些?如何查找大文件、查看日志中的关键字、查看端口占用?(基本
必考|考察实操)
35.Git操作问题:gitrebase和gitmerge有什么区别?发生代码冲突怎么解决?(常问|考
察实操)
36.Docker的镜像分层原理了解吗?Kubernetes(K8s)的Pod和Service概念解释一下。(常
问|网友分享)
37.算法题:反转链表/判断链表是否有环。(基本必考|学员真题)
38.算法题:无重复字符的最长子串(滑动窗口)。(重点准备|学员真题)
39.算法题:TopK问题(堆排序或快速选择算法)。(常问|需深度思考)
40.算法题:二叉树的层序遍历/二叉树的最大深度。(基本必考|学员真题)
41.场景设计:如何设计一个高并发的秒杀系统?(前端、网关、后端、数据库各层怎么做)
(极高频|需深度思考)
42.场景设计:如何设计一个全局唯一的ID生成器?(雪花算法Snowflake原理)(重点准备|
考察实操)
43.场景设计:短URL生成系统怎么设计?长短链接如何映射?(常问|网友分享)
44.数据库主从复制的延迟问题怎么解决?(常问|需深度思考)
45.在项目中遇到过最难的技术挑战是什么?你是如何一步步解决的?(极高频|考察软实
力)
46.你的项目是如何做压测的?QPS和TPS大概是多少?瓶颈在哪里?(重点准备|考察实
操)
47.什么是ThreadLocal?它为什么会导致内存泄漏?(常问|需深度思考)
48.Java8StreamAPI常用方法有哪些?了解Lambda表达式的闭包特性吗?(基本必考|背
诵即可)
49.讲讲MyBatis中#{}和${}的区别?如何防止SQL注入?(基本必考|背诵即可)
50.Maven依赖冲突(Jar包冲突)通常怎么解决?(常问|考察实操)
51.什么是零拷贝(ZeroCopy)?Sendfile和mmap的区别是什么?(常问|需深度思考)
52.JDK19/21引入的虚拟线程(VirtualThreads)了解吗?它和传统线程的区别?(常问|
网友分享)
53.如果产品经理提了一个不合理的需求,或者技术上实现成本极高,你会怎么处理?(常
问|考察软实力)
54.你平时通过什么渠道学习新技术?最近在看什么技术书或源码?(常问|考察软实力)
55.面对老旧系统的“屎山”代码,你是选择重构还是维持现状?如何降低重构风险?(常问|
考察抗压)
56.什么是接口幂等性?在你的项目中是如何保证接口幂等的?(重点准备|考察实操)
57.CI/CD(持续集成/持续部署)流程了解吗?你们团队的代码上线流程是怎样的?(常问|
网友分享)
58.未来3-5年的职业规划是什么?是想走技术专家路线还是管理路线?(基本必考|考察软实
力)
59.为什么从上一家公司离职?(基本必考|考察软实力)
60.我问完了,你有什么想问我的吗?(面试收尾)
【Java后端开发工程师】高频面试题深度解答
Q1:请做一个自我介绍
❌不好的回答示例:
面试官好,我叫张三,毕业于XX大学计算机专业。我平时性格比较开朗,能吃苦耐
劳。在大学期间学过Java、MySQL和SpringBoot,也做过一些课程设计,比如图
书管理系统。我对技术很有热情,平时也喜欢逛逛技术博客。我上一家公司是在一
家外包公司做增删改查,觉得成长不够,所以想来咱们公司试试。希望能给我一个
机会,我一定会努力工作的。
为什么这么回答不好:
1.流水账且无亮点:仅罗列了基础信息和性格,提到的“图书管理系统”是烂大街的校招项
目,无法体现社招求职者的专业深度。
2.技术栈描述空洞:只是泛泛地说“学过”,没有提及掌握程度(如熟悉、精通)以及具体的
实战应用场景。
3.暴露负面动机:直接说上一家是“外包”且“成长不够”,虽然诚实但容易被贴上技术底子薄
的标签,且显得不仅没有沉淀,反而急于逃离。
高分回答示例:
面试官好,我叫张三,拥有5年Java后端开发经验,目前专注于高并发系统的设计
与优化。我最核心的竞争力体现在三个方面:
第一,扎实的技术底座与架构能力。我精通Java并发编程与JVM调优,对Spring
Cloud微服务生态有深入的落地经验。在上一家公司,我主导了核心交易链路的重
构,将系统从单体架构迁移至微服务架构,成功支撑了日均500万的订单量,系统
可用性从99.9%提升至99.99%。
第二,具备复杂场景下的数据库优化经验。我非常擅长MySQL的索引调优和
Redis缓存策略设计。曾在一个高流量的营销活动中,通过引入RedisCluster和
Lua脚本解决“超卖”问题,并结合MySQL的分库分表方案(ShardingSphere),
解决了单表数据量过亿带来的查询延迟,将核心接口响应时间从500ms降低至
50ms以内。
第三,拥有良好的工程素养与团队推动力。我习惯阅读开源源码(如RocketMQ、
Dubbo)来解决疑难杂症,并在团队内部推广了CI/CD自动化流程和代码Review机
制,有效降低了线上故障率。
我对贵公司的技术氛围非常向往,特别是贵司在电商领域的业务挑战与我的技术栈
高度匹配。希望能有机会加入团队,用我的经验为业务创造价值。
Q2:聊聊HashMap的底层实现,JDK1.7和1.8有什么区别?为什么1.8要引入
红黑树?
❌不好的回答示例:
HashMap底层就是数组加链表。JDK1.7的时候只有数组和链表,插入的时候是用
头插法。JDK1.8变成了数组加链表加红黑树,插入改成了尾插法。引入红黑树是
因为链表太长了查询会变慢,红黑树查询快一点。另外,1.8里的扩容机制好像也不
太一样,计算哈希值的方法也优化了。其他的细节记不太清了,但在项目里我主要
是用它来存键值对的。
为什么这么回答不好:
1.关键点缺失:虽然提到了结构变化,但对于“为什么引入红黑树”的阈值(8转树,6退
链)以及时间复杂度变化(O(n)到O(logn))没有量化说明。
2.逻辑表述不清:对扩容机制的改变(如1.7的死循环问题、1.8的位运算优化)一笔带
过,无法展示对底层原理的深度理解。
3.缺乏专业术语:只是简单说“快一点”,没有从数据结构的角度解释性能提升的本质,显得
专业度不够。
高分回答示例:
HashMap是面试中的高频考点,我对它的源码有过深入研究。JDK1.8相比1.7主
要有三大核心变化:
1.底层数据结构的演进:
JDK1.7采用“数组+链表”结构。在极端情况下(Hash冲突严重),链表会变得
很长,导致查询时间复杂度退化为O(n)。
JDK1.8优化为“数组+链表+红黑树”。当链表长度超过8且数组长度大于64时,
链表会转为红黑树。这利用了红黑树O(logn)的查询复杂度,解决了Hash冲突导
致的性能退化问题。当节点数降至6以下时,又会退化为链表以节省空间。
2.插入方式的改变:
JDK1.7使用头插法,这在多线程扩容时容易导致链表成环,造成死循环。
JDK1.8改为尾插法,保证了元素插入顺序,彻底解决了并发扩容下的死循环问
题(尽管HashMap本身仍不是线程安全的)。
3.扩容机制的优化:
JDK1.7扩容时需要重新计算每个元素的Hash值(Rehash),效率较低。
JDK1.8利用了数组长度是2的幂次方的特性,通过(e.hash&oldCap)的结果判
断元素位置。如果结果为0,位置不变;如果不为0,位置为“原索引+旧数组长
度”。这种设计直接利用位运算代替了取模运算,极大提升了扩容效率。
Q3:ConcurrentHashMap是如何保证线程安全的?CAS+synchronized是
怎么配合的?
❌不好的回答示例:
ConcurrentHashMap在1.7的时候是用分段锁Segment,把大数组切成小段,每段
配一把锁。到了1.8就不用Segment了,改成了CAS和synchronized。插入数据的
时候先用CAS尝试一下,如果失败了就加synchronized锁。这样锁的粒度更细,性
能更好。具体怎么配合的,好像是锁住链表的头节点吧,反正比Hashtable那种全
表锁要快很多。
为什么这么回答不好:
1.细节模糊:对1.8中具体的加锁对象(Node节点)和CAS的使用场景(初始化数组、put
空节点)描述不清。
2.缺乏深度:没有解释清楚为什么1.8要放弃Segment(减少内存开销、提高并发度),也
没有提到扩容时的协同机制(多线程协助扩容)。
3.语言随意:“好像是”、“反正比...快”等词汇暴露了知识掌握的不确定性,难以获得面试官
信任。
高分回答示例:
ConcurrentHashMap在JDK1.8中进行了彻底的重构,放弃了1.7的Segment分段
锁,采用了“Node数组+CAS+synchronized”来实现更细粒度的并发控制:
1.降低锁粒度:
JDK1.7的锁粒度是Segment(默认为16个),并发度受限于Segment数量。
JDK1.8将锁粒度细化到了每个Hash桶(Node链表的头节点或红黑树的根节
点)。这意味着只要Hash不冲突,多线程可以完全并行操作,理论并发度达到了
数组长度。
2.CAS与synchronized的协同工作(Put流程):
CAS阶段:当向一个空的Hash桶插入数据时,系统不需要加锁,而是直接利用CAS
(CompareAndSwap)操作尝试将新节点写入数组。如果成功则直接返回,这避免
了加锁的开销。
synchronized阶段:如果Hash桶不为空(发生了Hash冲突),或者CAS失败,说明
有并发争抢。此时,线程会使用synchronized锁住当前桶的头节点(Node),然后进
行链表追加或红黑树插入操作。
3.并发扩容优化:
ConcurrentHashMap还引入了多线程协同扩容机制。当一个线程发现Map正在
扩容(节点状态为MOVED)时,它不会阻塞,而是会协助其他线程一起进行数
据迁移(transfer),进一步利用了多核CPU的能力,显著提升了吞吐量。
Q4:请手写一个单例模式(DCL双重检查锁),并解释为什么需要volatile关键
字?
❌不好的回答示例:
Java
public
class
Singleton
{
private
static
Singleton
instance;
private
Singleton()
{}
public
static
Singleton
getInstance()
{
if
(instance
==
null)
{
synchronized
(Singleton.class)
{
if
(instance
==
null)
{
instance
=
new
Singleton();
}
}
}
return
instance;
}
}
这就是DCL写法。加两个if判断是为了提高效率,避免每次都加锁。volatile关键字
的话,好像是保证可见性的,如果不加的话,多线程环境可能会读到空对象吧,或
者是为了防止重排序,具体细节有点忘了。
为什么这么回答不好:
1.代码漏洞:示例代码中漏掉了核心的volatile关键字,这直接导致了DCL失效,属于
严重的编码错误。
2.原理不清:对volatile的作用解释模棱两可,没有准确指出“指令重排序”导致对象半初
始化的问题。
3.缺乏专业性:对于经典的八股文题目,无法精准背诵和解释,会给面试官留下基础不扎
实的印象。
高分回答示例:
Java
public
class
Singleton
{
//
必须使用
volatile
关键字
private
volatile
static
Singleton
instance;
private
Singleton()
{}
public
static
Singleton
getInstance()
{
//
第一重检查:若已实例化,则不需加锁,提升性能
if
(instance
==
null)
{
synchronized
(Singleton.class)
{
//
第二重检查:防止多线程并发突破第一层检查后重复创建
if
(instance
==
null)
{
instance
=
new
Singleton();
}
}
}
return
instance;
}
}
为什么必须加volatile?
这主要是为了禁止指令重排序。在Java中,instance=newSingleton()这行代
码并非原子操作,它在底层会被编译为三步:
1.分配内存空间(memory=allocate())。
2.初始化对象(ctorInstance(memory))。
3.将instance引用指向内存地址(instance=memory)。
如果不加volatile,编译器或CPU可能会发生重排序,将步骤变成1->3->2。
在多线程环境下,线程A执行完步骤3(引用已赋值,但对象未初始化)时,线程B
抢占CPU,在第一重检查中发现instance!=null,直接返回了这个半初始化状态
的对象。这会导致空指针异常或数据错误。volatile通过内存屏障彻底避免了这种重
排序风险。
Q5:线程池的7个核心参数分别是什么?生产环境如何合理配置线程池大小?
❌不好的回答示例:
7个参数主要是核心线程数、最大线程数、过期时间、单位、任务队列、线程工厂
和拒绝策略。配置的话,我一般看是CPU密集型还是IO密集型。如果是CPU密集
型,就设成CPU核数加1;如果是IO密集型,就设成CPU核数乘以2。这都是网上
说的通用公式,我在项目里一般就直接用JDK自带的FixedThreadPool,比较方
便,还没遇到过什么大问题。
为什么这么回答不好:
1.理论脱离实际:仅仅背诵了公式,但没有结合实际业务场景(如接口响应时间、流量波
动)进行分析。
2.存在隐患:提到使用FixedThreadPool,这是阿里巴巴开发手册明确禁止的(因为无界
队列会导致OOM),暴露了实战经验的不足。
3.缺乏调优思路:没有考虑到动态调整或监控,生产环境的配置远比简单公式复杂。
高分回答示例:
线程池的7个参数包括:corePoolSize(核心线程数)、maximumPoolSize(最大线
程数)、keepAliveTime(存活时间)、unit(时间单位)、workQueue(阻塞队
列)、threadFactory(线程工厂)、handler(拒绝策略)。
在生产环境中配置线程池,我不会生搬硬套公式,而是遵循以下“压测+动态调
整”的策略:
1.初步估算:
CPU密集型任务(如加密、计算):N+1,尽量利用CPU。
IO密集型任务(如数据库、RPC调用):2N或N/(1-阻塞系数)。由于IO操作
不占用CPU,需要更多线程来提高并发度。
2.严控队列与拒绝策略:
绝对不使用Executors创建的线程池,因为LinkedBlockingQueue默认是无界
的,容易导致OOM。我会使用ThreadPoolExecutor,配合
ArrayBlockingQueue(有界队列),并根据业务重要性选择CallerRunsPolicy
(主线程执行)或AbortPolicy(抛异常)作为拒绝策略,防止系统崩溃。
3.动态监控与调优:
最重要的是,我会引入动态线程池(如Hippo4j或Nacos配置中心)。在线上,
我会监控线程池的活跃度、队列堆积情况。如果发现队列经常满,我会动态调大
maximumPoolSize;如果发现CPU利用率过高,则适当调小。任何配置都必须
基于真实的压测数据和线上监控来定。
Q6:什么是AQS(AbstractQueuedSynchronizer)?ReentrantLock的加锁
和释放锁流程是怎样的?
❌不好的回答示例:
AQS就是一个抽象队列同步器,它是很多锁的基础,比如ReentrantLock和
CountDownLatch都用到了它。它里面有一个state变量,还有一个FIFO队列。加
锁的时候就是去改那个state,改成功了就拿到锁了,失败了就进队列排队。释放锁
的时候就把state改回去,然后唤醒队列里的下一个线程。原理大概就是这样,具体
的源码我没太细看。
为什么这么回答不好:
1.描述过于浅显:只是泛泛而谈,没有涉及到CAS操作、CLH队列变体、独占/共享模式等
核心概念。
2.流程缺失:对于ReentrantLock的“可重入性”是如何实现的,以及公平锁与非公平锁在流
程上的区别完全没提。
3.态度问题:“源码没太细看”对于高级开发岗位来说是一个明显的减分项,暗示缺乏钻研精
神。
高分回答示例:
AQS(AbstractQueuedSynchronizer)是Java并发包(JUC)的基石,它提供了
一套通用的机制来管理同步状态(state)和线程排队(CLH队列变体)。
以ReentrantLock(非公平锁)为例,其加锁和释放锁的流程如下:
1.加锁流程(lock):
CAS抢占:线程进来后,直接通过CAS尝试将state从0修改为1。如果成功,直接
获取锁,设置当前拥有锁的线程为自己(这是非公平锁的特性)。
重入判断:如果CAS失败,判断当前持有锁的线程是否是自己。如果是,state加
1,实现可重入。
入队挂起:如果都不是,线程会被封装成Node节点,放入AQS的双向链表尾部。随
后,线程会通过LockSupport.park()挂起,等待被唤醒。
2.释放锁流程(unlock):
修改状态:线程将state减1。如果减后state为0,说明锁完全释放。
唤醒后继:锁完全释放后,从AQS队列的头部(Head)找到下一个有效的等待节点
(WaitStatus不为取消状态),调用LockSupport.unpark()唤醒该线程,让它去尝试
获取锁。
AQS通过模板方法模式,将复杂的排队和阻塞逻辑封装在底层,子类只需实现tryA
cquire和tryRelease等方法即可,体现了极高的设计美学。
Q7:讲一下JVM的内存模型(JMM),堆和栈的区别是什么?
❌不好的回答示例:
JVM内存模型主要包括堆、栈、方法区、程序计数器和本地方法栈。堆是存对象
的,最大的那一块。栈是存局部变量的,方法执行的时候会压栈。区别就是堆是线
程共享的,栈是线程私有的。还有就是堆会发生垃圾回收,栈一般不会。JMM的
话,好像是说主内存和工作内存什么的,为了保证线程安全。
为什么这么回答不好:
1.概念混淆:题目问的是JVM内存模型(JMM,JavaMemoryModel,涉及并发、可见
性),回答者却在讲运行时数据区(RuntimeDataArea),这是两个完全不同的概念。
2.深度不足:对堆栈的区别仅停留在“存对象”和“线程共享”层面,未涉及生命周期、空间大
小限制及抛出异常类型(OOMvsStackOverflow)。
3.表达不严谨:对JMM的描述含糊不清,没有点出原子性、可见性、有序性这三大特性。
高分回答示例:
这个问题需要区分运行时数据区和JMM(Java内存模型)。
首先,运行时数据区包括:
1.堆(Heap):线程共享,主要存放对象实例和数组,是垃圾回收(GC)的主要区域。
2.栈(Stack):线程私有,存放局部变量表、操作数栈等。方法调用对应栈帧入栈,结
束对应出栈。
3.方法区、程序计数器、本地方法栈等。
堆和栈的核心区别:
物理存储:堆内存较大且不连续,栈内存较小且连续。
生命周期:堆与JVM进程同生共死,栈随线程的生命周期结束而释放。
异常类型:堆空间不足抛OutOfMemoryError,栈深度不够抛StackOverflowError。
其次,JMM(JavaMemoryModel)是一套规范,用于屏蔽硬件差异,解决多线
程通信问题。它规定了:
1.所有变量存储在主内存。
2.每个线程有自己的工作内存,保存了变量的副本。
3.线程操作变量必须在工作内存中进行,不能直接读写主内存。
这就引入了可见性(volatile)、原子性(synchronized/CAS)和有序性
(Happens-Before原则)的问题,是并发编程的理论基础。
Q8:详细介绍一下CMS和G1垃圾收集器的区别,ZGC了解吗?
❌不好的回答示例:
CMS是老年代的收集器,用的是标记-清除算法。它的优点是并发收集,停顿时间
短,但是会产生内存碎片。G1是现在默认的收集器,它把堆分成了很多小块
(Region),不分年代了。G1可以预测停顿时间,性能更好。ZGC是最新的,号
称停顿时间不超过10ms,但我没在生产上用过。
为什么这么回答不好:
1.理解偏差:说G1“不分年代了”是错误的,G1依然保留了分代概念(逻辑分代,物理分
区)。
2.细节缺失:没有对比两者的核心痛点,如CMS的“浮动垃圾”和“ConcurrentMode
Failure”,以及G1的筛选回收(MixedGC)机制。
3.ZGC描述浅薄:仅提到了停顿时间,没有触及ZGC的核心技术(如读屏障、染色指
针)。
高分回答示例:
CMS和G1代表了垃圾回收器从“物理分代”向“逻辑分代、物理分区”的演进方向:
1.CMS(ConcurrentMarkSweep):
特点:追求最短停顿时间(LowPause),采用“标记-清除”算法。
缺点:会产生大量内存碎片,导致大对象分配困难;对CPU资源敏感;存在“浮动垃
圾”问题,若预留空间不足会触发FullGC(SerialOld),导致长时间停顿。
2.G1(Garbage-First):
特点:将堆划分为多个大小相等的Region,虽然保留了分代概念,但物理上不再隔
离。它能建立“可预测的停顿时间模型”,根据用户设定的目标时间,优先回收价值
(垃圾占比)最大的Region。
优势:采用“标记-整理”算法(Region之间是复制),不会产生内存碎片;适合大内存
机器(6G-8G以上)。
3.ZGC(ZGarbageCollector):
黑科技:JDK11/15引入,基于染色指针(ColoredPointers)和读屏障(Load
Barriers)技术。
目标:在TB级堆内存下,将STW(StopTheWorld)时间控制在10ms以内。它实现
了并发整理,几乎所有阶段都是并发执行的,是未来低延迟服务的首选。
Q9:生产环境出现OOM(内存溢出)或CPU飙升到100%,你的排查思路和步
骤是什么?
❌不好的回答示例:
如果CPU飙升,我会先用top命令看看是哪个进程占用的。如果是Java进程,我就
重启一下服务试试,看能不能恢复。如果不行,就看看日志有没有报错。如果是
OOM,我就把堆内存调大一点,比如把Xmx参数改大。实在不行就让运维帮忙
dump一下堆内存,然后用MAT工具分析一下,看是哪个对象太多了。
为什么这么回答不好:
1.盲目重启:“重启一下试试”是典型的运维大忌,会丢失案发现场(线程栈、堆信息),导
致问题无法根治。
2.简单粗暴:“调大内存”只是缓解症状,如果是代码逻辑(如死循环、内存泄漏)问题,调
大内存只会推迟崩溃时间。
3.流程不规范:缺乏定位到具体“线程”和“代码行”的步骤,排查逻辑非常松散。
高分回答示例:
面对线上OOM或CPU100%的问题,必须保持冷静,保留现场并精准定位。我的
排查SOP如下:
针对CPU飙升到100%:
1.定位进程:使用top命令找到占用CPU最高的进程ID(PID)。
2.定位线程:使用top-Hp<PID>查看该进程下占用CPU最高的线程ID(TID)。
3.转换进制:将TID转换为十六进制(printf"%x\n"TID)。
4.定位代码:使用jstack<PID>|grep<十六进制TID>-A20打印堆栈信息。如果看
到是业务代码,通常是死循环或复杂算法;如果是GC线程,说明发生了频繁的FullGC。
针对OOM(内存溢出):
1.保护现场:确保启动参数配置了-XX:+HeapDumpOnOutOfMemoryError,这样OOM时会
自动生成Dump文件。
2.离线分析:获取Dump文件,使用MAT(MemoryAnalyzerTool)或JProfiler进行分
析。
3.查找病因:
查看Histogram,找出实例数量最多的类。
分析DominatorTree,找到占用内存最大的对象(大对象)。
查看GCRoots引用链,定位是哪个静态变量或缓存队列一直在持有对象导致无法回
收(内存泄漏)。
4.解决:修复代码泄露逻辑,或根据业务量合理调整堆内存大小。
Q10:类加载机制中的“双亲委派模型”是什么?如何破坏双亲委派?
❌不好的回答示例:
双亲委派就是加载类的时候,先让父加载器去加载,父加载器加载不了再自己加
载。这样是为了安全,防止核心类被篡改。比如Object类,大家都要用,必须由启
动类加载器加载。如果要破坏的话,好像是重写loadClass方法吧。Tomcat好像就
破坏了,因为它要部署很多应用,不同的应用可能有同名的类。
为什么这么回答不好:
1.表述不准:“父加载器”容易让人误解为继承关系,实际上是组合(Composition)关系。
2.深度不够:对“破坏”场景的解释不够具体,未提到SPI(ServiceProviderInterface)机制
(如JDBCDriver)也是破坏双亲委派的典型案例。
3.逻辑单一:仅提到了Tomcat,没有展开讲Tomcat为什么要破坏(Web应用隔离性),回
答略显单薄。
高分回答示例:
双亲委派模型是指:当一个类加载器收到类加载请求时,它首先不会自己去加载,
而是将请求委派给父类加载器。层层向上,直到最顶层的BootstrapClassLoader。
只有当父加载器反馈无法完成加载时,子加载器才会尝试自己加载。
核心作用:
1.沙箱安全:防止核心API库(如java.lang.Object)被恶意篡改。
2.避免重复加载:保证类的全局唯一性。
如何破坏双亲委派?
主要通过重写ClassLoader的loadClass方法(该方法包含了双亲委派的逻辑)来
实现。典型场景包括:
1.TomcatWeb容器:Tomcat需要解决Web应用之间的类隔离问题(App1用Spring4,
App2用Spring5)。它自定义了WebAppClassLoader,优先加载Web应用目录下
(WEB-INF/classes)的类,打破了双亲委派,只有核心类库才委托给上级。
2.JavaSPI机制(JDBC):DriverManager是核心类(rt.jar),由启动类加载器加载。
但它需要加载厂商提供的驱动实现(如com.mysql.jdbc.Driver,在classpath下)。由
于启动类加载器看不见classpath,Java利用线程上下文类加载器
(ThreadContextClassLoader)完成了父级加载器去请求子级加载器完成类加载的动
作,这是一种反向委派。
Q11:MySQL索引的数据结构为什么选择B+树而不是B树或Hash?
❌不好的回答示例:
因为B+树查询快啊。Hash索引只支持等值查询,不支持范围查询,所以很少用。B
树的话,它的节点里也存数据,这就导致一个页存不了多少索引,树就会很高。
B+树只有叶子节点存数据,非叶子节点只存索引,树比较矮,IO次数少,所以效率
高。而且B+树叶子节点有链表,适合范围查询。
为什么这么回答不好:
1.回答不够条理:虽然点到了核心,但缺乏结构化陈述,像是在“挤牙膏”。
2.细节缺失:没有提到MySQL页大小(默认16KB)与B+树高度的具体估算关系(一般3层
能存2000万数据)。
3.专业度不足:对于“局部性原理”和“磁盘预读”等操作系统层面的支撑理论提及较少。
高分回答示例:
MySQLInnoDB引擎选择B+树作为索引结构,是基于磁盘I/O特性和查询场景的综
合考量:
1.与Hash对比(范围查询瓶颈):
Hash索引虽然等值查询仅需O(1),但它是无序的,无法支持范围查询(>、<、
between)和排序操作,也不能利用最左前缀匹配,因此不适合作为通用索引。
2.与B树对比(树高与I/O):
存储效率:B树的每个节点都存储Key和Value(数据),而MySQL页大小固定
(16KB)。如果数据较大,一个页能存的索引项就很少,导致树很高,意味着更多的
磁盘I/O。而B+树仅在叶子节点存数据,非叶子节点只存Key,这使得单一节点能容纳
更多索引,3-4层的高度就能支撑千万级数据,极大减少了I/O次数。
扫库能力:B树进行全表扫描需要遍历整棵树,效率低。B+树的叶子节点通过双向链
表连接,天然有序,非常适合范围查询和全表扫描(OrderBy/Limit)。
综上,B+树在保持低树高的同时,完美支持了关系型数据库最常见的范围和排序需
求。
Q12:谈谈MySQL事务的隔离级别,以及MVCC(多版本并发控制)的实现原
理。
❌不好的回答示例:
MySQL有四个隔离级别:读未提交、读已提交、可重复读、串行化。默认是可重复
读(RR)。
读未提交会有脏读;读已提交会有不可重复读;RR解决了不可重复读,但是会有幻
读。串行化最安全但最慢。
MVCC就是多版本并发控制,主要是为了提高并发。它在每行数据后面加了两个隐
藏列,记录创建版本号和删除版本号。读取的时候根据版本号判断能不能读。
为什么这么回答不好:
1.幻读问题表述不准:在InnoDB的RR级别下,通过MVCC+Next-KeyLock实际上已经基本
解决了幻读问题,回答中说“会有幻读”是不准确的。
2.MVCC原理陈旧:“创建/删除版本号”是网上过时的老教材说法。真正的InnoDB实现是基
于UndoLog链和ReadView。
3.缺乏深度:没有解释当前读(CurrentRead)和快照读(SnapshotRead)的区别。
高分回答示例:
MySQL的四个隔离级别中,RepeatableRead(RR)是默认级别。
MVCC(多版本并发控制)是InnoDB在RC和RR级别下实现快照读(非阻塞读)
的核心机制。它的实现依赖于三个隐式字段、UndoLog和ReadView:
1.UndoLog(回滚日志):
当修改记录时,MySQL不会直接覆盖原数据,而是将旧数据写入UndoLog,并
通过DB_ROLL_PTR指针将新老数据连接成一个版本链。
2.ReadView(读视图):
ReadView是事务进行快照读时产生的读视图,包含当前活跃事务ID列表
(m_ids)。
在RC级别下:每次Select查询都会生成一个新的ReadView,所以能读到其他事务已
提交的最新修改(导致不可重复读)。
在RR级别下:只有第一次Select会生成ReadView,后续查询复用该视图。这就保证
了在同一个事务中,多次读取看到的数据是一致的(解决了不可重复读)。
3.解决幻读:
MVCC解决了“快照读”下的幻读;而对于“当前读”(如selectforupdate),
InnoDB通过Next-KeyLock(间隙锁+行锁)锁定范围,防止其他事务插入新
行,从而彻底解决了幻读问题。
Q13:这是一条执行很慢的SQL,你会如何进行分析和优化?(Explain关键字
解读)
❌不好的回答示例:
如果SQL慢,我会先看日志,然后用Explain命令去分析。主要看type那列,如果
是ALL就是全表扫描,肯定慢,要加索引。还有看key那列,看有没有用到索引。如
果rows太大也不行。优化的话就是加索引,或者改写SQL,比如不要用select*,
不要在where条件里做计算。如果还慢,就得看是不是表太大了,可能要分库分
表。
为什么这么回答不好:
1.Explain解读不全:漏掉了核心字段Extra,其中的Usingfilesort和Usingtempora
ry往往是性能杀手。
2.分析逻辑单一:只是简单说“加索引”,没有考虑到索引失效的场景(如最左前缀、类型转
换)。
3.缺乏实操感:没有提到开启慢查询日志(SlowQueryLog)的第一步动作,直接跳到了
Explain。
高分回答示例:
面对慢SQL,我的优化SOP如下:
1.定位问题:
首先通过开启慢查询日志捕获慢SQL,或使用监控工具(如Arthas、
Prometheus)定位耗时语句。
2.Explain分析执行计划:
重点关注以下几个字段:
type:访问类型。优选ref、range,坚决避免ALL(全表扫描)和index(全
索引扫描)。
key:实际使用的索引。如果为NULL,说明索引未生效,需检查是否违反最左前缀原
则、使用了!=、或者在字段上进行了函数计算。
rows:扫描行数。数值越大,IO开销越大。
Extra:额外信息。出现Usingfilesort(文件排序)或Usingtemporary(临
时表)是性能大忌,必须优化排序或分组逻辑。
3.优化手段:
索引优化:覆盖索引(CoveringIndex)是神器,尽量让查询字段都在索引中,避免
回表。
SQL改写:例如将select*改为具体字段;用limit分页优化大数据量偏移
(延迟关联);小表驱动大表。
架构层面:如果单表数据量过千万,物理优化无效,我会考虑读写分离或分库分表。
Q14:MySQL的锁机制了解吗?什么是间隙锁(GapLock),如何避免死锁?
❌不好的回答示例:
MySQL锁主要有表锁和行锁。MyISAM用表锁,InnoDB用行锁。行锁有共享锁和
排他锁。间隙锁就是锁住两条记录中间的空隙,防止别人插入数据,是为了解决幻
读的。避免死锁的话,就是尽量按顺序加锁,或者大事务拆成小事务。如果发生了
死锁,MySQL会自动回滚其中一个事务。
为什么这么回答不好:
1.锁的分类不全:漏掉了意向锁(IntentionLock)、临键锁(Next-KeyLock)等重要概
念。
2.间隙锁触发条件模糊:没说明间隙锁是在RR隔离级别下、且未命中唯一索引(或范围查
询)时才会触发。
3.死锁解决太浅:仅提到了顺序加锁,没有涉及具体的业务场景,比如批量更新时的ID排
序处理。
高分回答示例:
InnoDB的锁机制非常丰富,主要包含:
1.记录锁(RecordLock):锁住具体的索引行。
2.间隙锁(GapLock):锁住两个索引之间的区间(开区间),主要用于解决幻读。
3.临键锁(Next-KeyLock):记录锁+间隙锁(左开右闭),是RR级别下的默认行锁
算法。
关于间隙锁(GapLock):
它在RepeatableRead隔离级别下生效。当使用范围查询,或者等值查询未命中唯
一索引时,InnoDB不仅锁住数据,还会锁住数据周边的空隙,防止其他事务插入新
数据(Insert),从而避免幻读。这虽然保证了数据一致性,但也降低了并发度。
如何避免死锁:
死锁通常是因为两个事务以不同顺序锁定资源造成的。在业务中,我通常采取以下
策略:
1.固定顺序加锁:比如批量更新库存,我会先将商品ID在代码层面进行排序,然后按顺序
执行Update,打破环路等待条件。
2.大事务拆解:将长事务拆分为多个短事务,减少持有锁的时间。
3.索引优化:确保Update语句走索引,避免因索引失效导致锁升级(行锁变表锁)或锁范
围扩大,增加死锁概率。
Q15:Redis有哪些数据类型?在你的项目中具体用在什么场景?
❌不好的回答示例:
Redis有5种基本类型:String、List、Set、Hash、Zset。
String用得最多,存Session或者缓存用户信息。List可以做消息队列。Set可以去
重。Hash存对象。Zset可以做排行榜。
这就是我们项目里主要用的,其他的像Bitmap什么的我没怎么用过。
为什么这么回答不好:
1.回答千篇一律:这是最标准的“背书式”回答,没有任何亮点,无法区分求职者水平。
2.场景缺乏深度:仅说了“存对象”、“做排行榜”,没有结合具体业务痛点(如:为什么要用
Hash存对象而不用JSONString?)。
3.高级类型缺失:对于Bitmap、HyperLogLog、Geo等高级数据结构只字未提,错失了展
示技术广度的机会。
高分回答示例:
在我的项目中,除了基础的KV缓存,我深入使用了Redis的多种数据结构来解决具
体业务问题:
1.String:不仅用于常规缓存,我还利用其原子递增(INCR)特性实现了分布式ID生成和
接口限流(结合过期时间)。
2.Hash:我用于存储电商购物车数据。Key是用户ID,Field是商品ID,Value是数量。相
比JSONString,Hash能单独修改某个商品数量,减少了序列化开销和网络传输量。
3.ZSet(SortedSet):用于实现延时队列。将任务执行时间戳作为Score,轮询获取
Score小于当前时间的任务进行消费。同时也用于实现热搜排行榜。
4.Bitmap:这是一个亮点。我用它实现了用户签到功能。一个用户一年的签到记录只需不
到4KB内存(365bit),极大地节省了存储空间。
5.HyperLogLog:用于统计网站的UV(独立访客)。在允许极小误差的情况下,它能用
12KB内存统计上亿级别的数据,替代了传统的Set去重方案。
Q16:Redis的持久化机制RDB和AOF有什么区别?生产环境怎么选?
❌不好的回答示例:
RDB是快照,把内存数据dump到磁盘,文件小,恢复快,但是会丢数据。AOF是
日志,记录每条写命令,数据全,但是文件大,恢复慢。
生产环境一般两个都开。或者只开RDB,因为AOF太慢了。如果怕丢数据就开
AOF。
为什么这么回答不好:
1.理解片面:说“AOF太慢”是不准确的,AOF的fsync策略(everysec)性能损耗是可以接
受的。
2.配置策略过时:仅仅说“两个都开”,没有提到Redis4.0引入的混合持久化机制,这是目
前的最佳实践。
3.缺乏恢复场景分析:没有提到RDB在灾难恢复(备份到远程)中的优势。
高分回答示例:
Redis提供RDB和AOF两种持久化机制,它们各有优劣:
1.RDB(快照):
原理:在指定时间间隔内生成数据集的时间点快照(fork子进程,写时复制COW)。
优点:文件紧凑,适合全量备份和灾难恢复;恢复速度快。
缺点:无法实时持久化,服务器宕机会丢失最后一次快照后的数据。
2.AOF(追加文件):
原理:以日志形式记录每次写操作,重启时重放命令恢复数据。
优点:数据安全性高(支持秒级甚至同步刷盘),不丢数据。
缺点:文件体积大,重放速度慢于RDB。
生产环境最佳实践:
虽然官方建议同时开启,但在Redis4.0之后,我强烈推荐使用混合持久化(RDB
+AOF)。
配置aof-use-rdb-preambleyes。在AOF重写时,将内存数据以RDB格式写入
AOF文件头部,之后的增量数据以AOF文本追加。这样既享受了RDB的快速加载,
又拥有了AOF的数据安全性,是目前的最优解。
Q17:什么是缓存穿透、缓存击穿和缓存雪崩?分别有什么解决方案?
❌不好的回答示例:
缓存穿透就是查不到数据,一直打数据库。解决办法是校验参数,或者用布隆过滤
器。
缓存击穿就是热点Key失效了,大量请求打过来。解决办法是永不过期,或者加
锁。
缓存雪崩就是大量Key同时失效。解决办法是加随机过期时间,或者搞集群。
这三个问题很常见,背下来就行了。
为什么这么回答不好:
1.定义区分度不够:容易把穿透和击穿搞混,描述过于口语化。
2.解决方案单一:对于“缓存击穿”,只提到了互斥锁,没提到逻辑过期;对于“穿透”,没提
到空对象缓存。
3.缺乏实战细节:比如布隆过滤器的误判问题、加锁的具体实现(分布式锁)没有展开。
高分回答示例:
这三个是缓存系统的高频风险点,我的理解和处理方案如下:
1.缓存穿透(查无此据):
现象:请求根本不存在的数据(如ID=-1),缓存和数据库都查不到,导致请求直透
DB。
解决:*缓存空对象:即使DB查不到,也存一个null值到Redis并设较短过期时间。
布隆过滤器(BloomFilter):在请求到达缓存前,先通过位图判断Key是否存
在。虽然有误判率,但能拦截绝大多数非法请求。
2.缓存击穿(热点失效):
现象:某个极度热点Key(如秒杀商品)突然过期,高并发请求瞬间击穿缓存打垮
DB。
解决:
互斥锁(Mutex):缓存失效时,只允许一个线程去查DB重建缓存,其他线程等
待。
逻辑过期:永不过期,但Value中包含时间戳。后台线程异步刷新数据,保证高可
用。
3.缓存雪崩(集体崩塌):
现象:大量Key在同一时刻失效,或Redis宕机。
解决:
随机TTL:在设置过期时间时添加随机值(如1-5分钟),打散失效时间。
高可用架构:搭建RedisSentinel或Cluster集群,防止单点故障。
多级缓存:引入Caffeine本地缓存作为一级屏障。
Q18:如何实现分布式锁?基于Redis(Redisson)和Zookeeper实现的区别
是什么?
❌不好的回答示例:
用Redis实现就是setnx,如果返回1就拿到锁了。用完del删掉。为了防止死锁要加
过期时间。但是这有个问题,如果业务没执行完锁就过期了怎么办?所以可以用
Redisson,它有个看门狗。
Zookeeper就是创建临时节点,谁创建成功谁拿锁。ZK是强一致性的,Redis是
AP的。一般用Redis多一点,因为快。
为什么这么回答不好:
1.原生实现简陋:简单的setnx+expire不是原子操作,存在隐患(虽然新版Redis支
持原子命令,但回答未提及)。
2.Redisson原理浅:对“看门狗(Watchdog)”的机制解释不清(每10s续期)。
3.ZK对比不够深:没有提到ZK的“顺序节点+监听机制”避免惊群效应的优势。
高分回答示例:
在生产环境中,我通常直接使用Redisson框架来实现Redis分布式锁,因为它完
美解决了原生setnx的痛点:
1.Redisson原理(核心亮点):
原子性:底层使用Lua脚本保证加锁和设置过期时间的原子性。
看门狗机制(Watchdog):解决了“业务没跑完锁就过期”的难题。默认每10秒检测
一次,如果线程还持有锁,就自动延长过期时间,直到业务结束。
可重入性:结合Hash结构记录线程ID和重入次数,支持可重入。
2.RedisvsZookeeper对比:
性能(CPvsAP):Redis是AP模型,基于内存,性能极高(QPS10w+),但由
主从异步复制可能导致锁丢失(Master宕机时)。Zookeeper是CP模型,基于ZAB
协议保证强一致性,数据更安全,但写性能较差。
实现机制:ZK利用临时顺序节点+Watcher监听,实现了阻塞等待(公平锁),避免
了无效轮询。
选型建议:绝大多数互联网高并发场景选Redis(Redisson),因为并发性能是
瓶颈;只有对锁的可靠性要求极高(如金融核心转账)才考虑Zookeeper。
Q19:SpringBean的生命周期是怎样的?BeanPostProcessor在其中起什么
作用?
❌不好的回答示例:
Bean的生命周期就是实例化、属性赋值、初始化、销毁。
Spring启动的时候先扫描类,然后new一个对象,然后依赖注入,填充属性。然后
执行init-method方法。最后容器关闭的时候执行destroy方法。
BeanPostProcessor就是后置处理器,可以在初始化前后做一些事情,比如AOP
就是在后置处理器里做的。
为什么这么回答不好:
1.流程粗糙:漏掉了非常关键的“Aware接口回调”步骤(如BeanNameAware)。
2.BPP作用不全:仅提到了AOP,没提到它在Spring内部扩展中的核心地位(如注解处
理)。
3.缺乏宏观视角:没区分“实例化(Instantiation)”和“初始化(Initialization)”这两个极易混
淆的概念。
高分回答示例:
SpringBean的生命周期非常复杂,但我习惯将其概括为“四步走+扩展点”:
1.实例化(Instantiation):
Spring利用反射(CreateBeanInstance)在堆中申请内存,创建Bean的原始对
象(此时属性还是null)。
2.属性赋值(PopulateBean):
进行依赖注入(DI),将@Autowired或配置的属性填充到Bean中。
3.初始化(Initialization)——核心逻辑集中区:
Aware接口回调:注入BeanName、BeanFactory等容器资源。
BeanPostProcessor(Before):执行postProcessBeforeInitialization。
初始化方法:执行@PostConstruct、InitializingBean接口方法、xml中的init-m
ethod。
BeanPostProcessor(After):执行postProcessAfterInitialization。AOP代理
对象通常就是在这个阶段生成的(如果需要代理,这里返回的就是Proxy对象而非原始
对象)。
4.销毁(Destruction):
容器关闭时,执行@PreDestroy或DisposableBean接口方法。
BeanPostProcessor的作用:
它是Spring最重要的扩展点(Hook)。AOP动态代理、@Value注解解析、
@Autowired依赖注入等核心功能,本质上都是通过实现不同的
BeanPostProcessor来介入Bean生命周期完成的。
Q20:Spring是如何解决循环依赖问题的?三级缓存的作用分别是什么?
❌不好的回答示例:
循环依赖就是A依赖B,B依赖A。Spring用三级缓存解决的。
第一级存成品对象,第二级存半成品,第三级存工厂。
创建A的时候,先把A放到缓存里,然后去创建B,B发现A在缓存里,就直接拿来用
了,这样就解决了。但是构造器循环依赖解决不了。
为什么这么回答不好:
1.逻辑断层:没解释清楚为什么需要第三级缓存?直接用两级为什么不行?(核心在于
AOP代理对象的引用问题)。
2.对象状态描述不清:什么是“成品”?什么是“半成品”?专业术语应该是“完全初始化的
Bean”和“早期引用”。
3.缺乏场景限制:没强调Spring只能解决单例(Singleton)且Setter注入的循环依赖。
高分回答示例:
Spring通过三级缓存解决了单例模式下Setter注入的循环依赖问题。其核心思想
是“提前暴露早期引用”。
三级缓存结构:
1.一级缓存(singletonObjects):存放经历了完整生命周期的、完全可用的单例Bean。
2.二级缓存(earlySingletonObjects):存放早期的Bean对象(属性还未填充),用于
中途被引用。
3.三级缓存(singletonFactories):存放ObjectFactory(函数式接口),用于生成
Bean的早期引用。
解决流程(A依赖B,B依赖A):
1.A实例化后,将一个lambda表达式(工厂)存入三级缓存。
2.A填充属性时发现需要B,去创建B。
3.B填充属性时发现需要A,依次查一级、二级缓存皆无,从三级缓存中获取A的工厂对
象,调用getObject()拿到A的早期引用(如果有AOP,此时会提前创建代理对象),
并放入二级缓存。
4.B完成初始化,存入一级缓存。
5.A拿到B,完成初始化,从二级缓存移动到一级缓存。
为什么需要第三级?
核心是为了解决AOP代理问题。如果直接将原始对象放入二级缓存,当A需要被代
理时,B引用到的将是原始A而不是代理A,违背了单例原则。三级缓存的工厂机制
延迟了代理对象的创建,只有在发生循环依赖时才提前生成代理对象。
Q21:SpringBoot的自动装配原理是什么?@SpringBootApplication注解背
后做了什么?
❌不好的回答示例:
SpringBoot自动装配就是不用写xml配置了,它自己能把Bean扫描进去。@Spring
BootApplication这个注解是一个组合注解,里面有@Configuration,还有开启自动
配置的注解。原理就是Spring启动的时候去classpath下找jar包,看有没有对应的
依赖,如果有就自动配置。比如引入了Redis包,它就自动配好RedisTemplate。
具体是读取哪个文件我忘了,好像是META-INF下面的一个什么文件。
为什么这么回答不好:
1.原理描述模糊:“找jar包”这种说法太通俗,没有点出核心机制——SPI(ServiceProvider
Interface)和条件注解。
2.关键文件名缺失:没有准确说出spring.factories(或SpringBoot3.0后的AutoConfi
guration.imports),这是自动装配的灵魂。
3.缺乏注解拆解:对@SpringBootApplication的三个核心组成部分(@SpringBootConfig
uration、@ComponentScan、@EnableAutoConfiguration)表述不全。
高分回答示例:
@SpringBootApplication是一个复合注解,其核心逻辑由三个注解支撑:@SpringBo
otConfiguration(声明配置类)、@ComponentScan(组件扫描)和最核心的**@En
ableAutoConfiguration**。
自动装配的原理可以概括为“SPI机制+条件注解”:
1.加载候选配置(SPI):
当SpringBoot启动时,@EnableAutoConfiguration会利用
SpringFactoriesLoader扫描所有Jar包下的META-INF/spring.factories文
件(SpringBoot3.0后改为META-INF/spring/....imports)。这个文件中列出
了所有可能需要自动装配的配置类(ConfigurationClass),比如
RedisAutoConfiguration。
2.筛选配置(去伪存真):
加载进来的几百个配置类不会全部生效,而是通过@Conditional系列注解进行
过滤。
例如RedisAutoConfiguration上标有@ConditionalOnClass(RedisOperations.cla
ss)。
只有当你项目中引入了spring-boot-starter-data-redis依赖,classpath中存在
RedisOperations类时,这个配置才会生效,RedisTemplate才会被注入容器。
3.结果:
这种“约定大于配置”的机制,让我们无需手动编写大量重复的Bean定义,真正实
现了开箱即用。
Q22:动态代理JDKProxy和CGLIB有什么区别?SpringAOP默认使用哪种?
❌不好的回答示例:
JDK代理是基于接口的,CGLIB是基于继承的。JDK代理是用反射做的,CGLIB是
用字节码技术做的。效率上来说,以前说CGLIB快,现在JDK快了。
SpringAOP默认是用JDK代理。如果类没有接口,就自动切到CGLIB。不过在
SpringBoot里好像有点不一样,好像默认都是CGLIB了。具体的配置参数我不记
得了。
为什么这么回答不好:
1.技术细节浅尝辄止:没有提到JDK代理的核心类Proxy和InvocationHandler,也没提
到CGLIB底层的ASM框架。
2.性能对比滞后:关于“快慢”的讨论缺乏JDK版本的前提,容易产生误导。
3.默认行为不确定:对SpringBoot2.x之后默认策略的变更(默认强制使用CGLIB)表述
含糊,显得对新特性关注不足。
高分回答示例:
JDK动态代理和CGLIB是Java实现代理模式的两种核心手段,主要区别如下:
1.实现原理:
JDKProxy:要求目标类必须实现
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 工程安全生产培训会议课件
- 2026年社区社会治安综合治理工作计划范文(4篇)
- 广东省东莞市2023-2024学年七年级上学期期末英语试卷(含答案)
- 慢病管理中的社会营销策略
- 慢病健康素养在医疗体系中的定位
- 慢病主动健康:环境因素干预策略
- 慢性肾病透析患者的个性化干预策略
- 《DLT 2148-2020生物质着火温度的测定方法》专题研究报告解读
- CN115040425A 一种含有工业大麻二酚的护肤液及其制备方法 (苏州工业园区安诺科斯化妆品研发有限公司)
- 工地工人进场安全培训课件
- 2025重庆市涪陵区马武镇人民政府选聘本土人才14人参考题库附答案
- 二年级上册语文试题-第六单元测试题-人教部编版(含答案)
- 医院院感考试题库及答案
- 拣货主管年终总结
- 糖尿病重症患者肠内营养血糖调控方案
- GB/T 15789-2005土工布及其有关产品无负荷时垂直渗透特性的测定
- GA/T 995-2020道路交通安全违法行为视频取证设备技术规范
- 化学工程与技术学科硕士研究生培养方案
- 最新人教版七年级英语上册全册复习课件
- 家庭农场认定申请表(表样)
- YY∕T 0296-2022 一次性使用注射针 识别色标
评论
0/150
提交评论