news 2026/8/30 7:34:38

2026版Java八股面试文:JVM、并发、Spring高频考点全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2026版Java八股面试文:JVM、并发、Spring高频考点全解析

先坦白说,这份2026版Java八股面试文,是我把近几年面过的人、自己被问过的题、以及身边大厂朋友反馈的高频考点重新梳理后整理的。大而全的八股清单网上很多,但真正带着答案、带着坑点、带着“为什么这么答”的版本很少,所以这篇万字长文值得你收藏起来,面试前翻一翻,胜过临时抱佛脚。

这篇文章覆盖Java基础、集合、JVM、并发、新特性、Spring、数据库、分布式、算法和设计模式,每个题都按“怎么答+为什么这么答+追问点”的套路来写。不管你是刚准备找实习的在校生,还是工作了几年想跳槽的Java工程师,这篇都能直接当复习提纲用。文章很长,建议先收藏再慢慢看。

1. Java基础核心题:面试官最爱的“送分题”与陷阱题

1.1 JDK、JRE、JVM的区别:一条必答题

这道题看起来简单,但每年都能刷掉一批人。标准答法是:JDK是Java开发工具包,包含编译器javac、调试器jdb等工具,还包含JRE;JRE是Java运行时环境,包含JVM和核心类库;JVM是Java虚拟机,负责把字节码解释或编译成机器码执行。

关键得分点在于,你要主动补充“Java为什么能跨平台”这个关联问题。跨平台靠的不是JDK也不是JRE,而是JVM本身。Java源码编译后生成的是class字节码,字节码不针对任何具体操作系统,而每一套操作系统上都有对应的JVM实现,JVM负责把字节码翻译成当前系统能识别的机器指令。一句话总结:Java一次编译到处运行,实际上是一次编译到处需要装JVM。

面试官如果往下追问“JVM和JDK是不是一回事”,你就得说明白:商用JDK里自带JVM实现(比如HotSpot),但JVM只是JDK众多组件中的一个,你把JDK删掉只留JVM也跑不了javac。

1.2 面向对象三大特性:别只背定义

封装、继承、多态,几乎每个Java面试者都会被问。但很多人只背定义,一问到“多态在JVM里怎么实现的”就卡壳。

封装的核心不是“把字段设为private”,而是对外暴露最小化接口,隐藏内部实现细节。判断封装好坏的标准是:调用方是否只依赖稳定的公开接口,而不需要关心内部状态怎么变化。

继承的本质是“is-a”关系,子类复用父类行为。但Java的继承有个坑,只支持单继承,这是为了规避菱形继承带来的方法歧义。很多面试者被问到“接口默认方法引入后,Java是否变相支持了多继承”,能答出“一个类可以实现多个接口,接口之间用default方法解决冲突规则”就算加分。

多态是三大特性里最容易被深挖的。需要答出三点:编译期看的是声明类型,调用方法时JVM根据实际对象类型做动态分派;方法重载是编译期多态,方法重写是运行期多态;多态存在的前提是继承或实现关系加方法重写。再往上追问,就是虚方法表、invokevirtual指令和动态绑定,这块放到JVM章节一起说。

1.3 ==、equals和hashCode的三角关系

这题Java面试基本是必考,尤其“为什么重写equals必须重写hashCode”这个问题,说清楚逻辑很关键。

==比较的是内存地址,equals默认调用的也是Object的==逻辑。Integer等包装类虽然没有重写equals的默认地址比较,但实际已经重写了equals去比较值。这里有个著名的坑:Integer在-128到127之间有缓存,用==比较缓存范围内的整数包装类会返回true,超出缓存范围就返回false。所以比较包装类数值,一律用equals,别用==。

hashCode和equals的关系是这样的:两个对象相等(equals结果为true),它们的hashCode必须相等;两个对象hashCode相等,equals不一定相等,因为存在哈希碰撞。这个约束是HashMap使用的核心前提:HashMap先算key的hash定位到桶,再用equals比对链表节点。如果你只重写equals不重写hashCode,明明业务上相等的对象会落到不同的桶里,导致HashMap里重复存放数据。

1.4 String、StringBuilder、StringBuffer怎么选

String是不可变的,每次拼接都会创建新对象;StringBuilder是可变字符序列,线程不安全但效率高;StringBuffer在StringBuilder基础上加了synchronized,线程安全但性能略低。这是基础答案。

面试官真正想看的是你有没有实际踩过性能坑。比如在循环里做字符串拼接,用String加号拼,每次循环都new出中间String对象,GC压力很大,改成StringBuilder后循环外创建一次builder,循环内append,性能差别在数据量大时非常明显。再追问“JVM会不会优化加号拼接”,你可以说JDK9之后javac会把字面量拼接优化为invokedynamic或StringConcatFactory,但循环内动态拼接仍然会有对象创建开销。

还有一个隐藏考点:String的intern方法。调用str.intern()可以在字符串常量池中查找是否已有相同内容的字符串,没有则入池并返回池中的引用。这个知识点在面试里经常结合“字符串常量池在JDK7之前放在方法区,JDK7之后移到堆里”一起问。

1.5 枚举与异常:被低估的送分题

Java枚举看起来简单,但很多人答不好。“枚举的底层本质是什么”标准答法是:枚举本质是一个继承了java.lang.Enum的final类,每个枚举常量是类的一个静态final实例。所以枚举可以写构造方法、成员变量和方法,而且枚举是天然的线程安全单例,因为类加载和枚举常量创建都由JVM保证只执行一次。

异常部分常问“受检异常和非受检异常的区别”。受检异常必须显式捕获或抛出,比如IOException、SQLException;非受检异常是RuntimeException的子类,比如NullPointerException、ArrayIndexOutOfBoundsException,编译器不强制处理。注意点有两个:不要捕获异常后只打日志不处理,也不要抛异常抛得比业务逻辑还多。很多人回答时把Error和Exception搞混,Error是JVM层面的严重问题,比如OutOfMemoryError,程序通常无法恢复,不应该catch。

2. 集合框架:HashMap是永远绕不过去的坎

2.1 HashMap底层结构与put流程

HashMap是Java面试的“题王”,面试官可以从数组聊到红黑树,再聊到线程安全。先背实底层结构:JDK8的HashMap是数组+链表+红黑树,数组里的每个元素叫桶,桶里存的是链表头节点或红黑树根节点。

put方法的流程步骤:先对key做hash运算,JDK8的扰动函数是key.hashCode()高16位异或低16位,这样能让高位信息参与路由,降低碰撞概率;然后根据hash与数组长度减一做与运算,算出桶下标;如果桶为空直接放;如果桶不为空就遍历链表或树,用equals比对key,存在则替换旧值,不存在则尾插法新增节点;链表长度达到8且数组长度达到64时,链表转红黑树。

追问环节常见题是“为什么HashMap的默认容量是16,负载因子是0.75”。容量的2次幂设计是为了让hash&(n-1)能等价于hash%n,且位运算更快。负载因子0.75是空间利用率和时间复杂度的折中:太大会增加碰撞概率,太小会频繁扩容。还有个关键点:HashMap的扩容是扩容到原来的两倍,扩容后元素要么在原位置,要么在原位置加旧数组长度,这个特性让重哈希处理起来非常高效,JDK8对扩容做了优化,不需要重新计算每个key的hash,只需判断新增的高位bit是0还是1。

2.2 HashMap为什么线程不安全

这个话题一定要会。JDK7的HashMap在并发扩容时可能出现环形链表,导致死循环,表现为CPU飙到100%;JDK8虽然改成了尾插法,不再存在死循环问题,但并发put还是会造成数据覆盖,比如两个线程同时算出同一个桶位,一个线程写进去的结果会被另一个线程覆盖掉。

解决办法是并发场景用ConcurrentHashMap。面试官常问“ConcurrentHashMap为什么不用给整个表加锁也能保证安全”。先答JDK7的实现:分段锁,把数据分成一段一段,每段配一把锁,只有操作同一段时才竞争。再答JDK8的实现:抛弃了分段锁,改为CAS+synchronized锁桶头节点,锁粒度更细,并发度更高。补充一条:ConcurrentHashMap的size()计算也有一套机制,先尝试无锁统计,竞争激烈时才加锁统计,避免扩容时的size统计不准确。

2.3 ArrayList与LinkedList怎么选

这个题表面上简单,核心区别就一句话:ArrayList基于动态数组,随机访问快;LinkedList基于双向链表,插入删除快(如果已经定位到节点)。但如果面试官往下追问就变难了。

追问一:数组和链表哪个更省内存?ArrayList的底层数组是连续内存,每个槽位只存引用;LinkedList每个节点除了数据还存前后两个指针,额外开销更大,而且节点内存不连续,对CPU缓存不友好。所以实际开发中绝大多数场景选ArrayList。

追问二:ArrayList插入元素一定会慢吗?不一定。在末尾插入,只要不需要扩容,就是纯数组赋值,非常快;在头部插入才需要移动大量元素。LinkedList在头部插入确实O(1),但前提是已经知道头节点。这道题面试官想听的是:不要背结论,要看数据规模、操作模式和内存局部性来选。

还有个隐藏点:ArrayList扩容机制是1.5倍,每次扩容都涉及Arrays.copyOf把旧数组数据拷到新数组。如果能在创建时预估数据规模,直接传初始容量,就能避免频繁扩容带来的拷贝开销。

3. JVM与内存:OOM排查是加分项

3.1 运行时数据区域划分

JVM内存模型这题,从热搜词里就能看出面试有多高频,比如“java: OutOfMemoryError: insufficient memory”就是一类非常典型的线上问题。答题先背区域划分:线程共享的有堆、方法区(JDK8之后改为元空间)、运行时常量池;线程私有的有虚拟机栈、本地方法栈、程序计数器。

堆是对象分配的主要区域,也是垃圾回收的主要区域,分新生代和老年代,新生代又分Eden区和两个Survivor区,默认比例8:1:1。虚拟机栈是线程私有的,每个方法调用对应一个栈帧,栈帧里存局部变量表、操作数栈、动态链接、方法返回地址。递归太深或者栈帧太大,会抛StackOverflowError。

元空间是JDK8的重要变化,它替代了永久代,存储类元数据。最大区别是元空间使用本地内存,不再是JVM堆的一部分,所以默认情况下元空间大小只受本机物理内存限制,不会出现永久代OOM,但也可能因为加载类太多把机器内存耗尽,线上可以设置-XX:MaxMetaspaceSize做限制。

3.2 垃圾回收算法与分代回收

GC这一块,先背四种基础算法:标记-清除,标记-复制,标记-整理,分代收集。标记-清除会产生内存碎片;复制算法用两块内存交替使用,不会碎片化但浪费空间;标记-整理适合老年代,把存活对象往一端移动。

然后答分代收集:新生代对象存活率低,用复制算法,Eden区满了就触发Minor GC,存活对象从Eden和Survivor复制到另一个Survivor,对象年龄达到15(-XX:MaxTenuringThreshold可调)才晋升老年代。老年代对象存活率高,用标记-整理或标记-清除,触发Major GC/Full GC。

再往上追问会问到具体垃圾收集器。CMS是JDK8比较常用的老年代收集器,目标是最短停顿时间,但有两个问题:并发回收阶段占用CPU,且标记-清除产生碎片,碎片严重时触发Full GC甚至Concurrent Mode Failure。G1是JDK9之后的默认收集器,把堆划分为多个大小相等的Region,通过跟踪每个Region的垃圾堆积价值,优先回收收益最大的Region,能设定期望停顿时间。ZGC是比较新的低延迟收集器,目标是把停顿时间控制在10ms以内,JDK15之后支持分代ZGC,适合超大堆场景。面试答到G1基本就够用了,ZGC留一句“了解”即可。

3.3 类加载过程与双亲委派

类加载机制既基础又常考。标准流程是加载、验证、准备、解析、初始化。加载阶段通过类的全限定名获取二进制字节流,并在堆中生成Class对象;验证阶段检查字节流是否符合JVM规范;准备阶段为静态变量分配内存并设置默认值;解析阶段把符号引用替换为直接引用;初始化阶段执行静态代码块和静态变量赋值。

双亲委派模型是关键。简单说:类加载请求先交给父加载器,父加载器找不到才交给子加载器。启动类加载器Bootstrap只加载java.*下的核心类,扩展类加载器Extension加载扩展包,应用类加载器Application加载classpath下的类。这样做有两个好处:防止核心类被自定义覆盖,避免同名类重复加载。

面试官常问“怎么打破双亲委派”,你可以提三种场景:自定义类加载器重写loadClass方法,比如Tomcat的WebAppClassLoader给每个应用独立类加载器实现应用隔离;JDBC的DriverManager用SPI机制,让启动类加载器加载的代码能调用应用类加载器加载的驱动实现;热部署框架的核心也是自定义类加载器,每次更新加载新的Class对象实现代码替换。实际项目中遇到NoClassDefFoundError或ClassNotFoundException,优先排查类加载器可见性,这个思路在线上排查时特别有用。

3.4 线上OOM排查实战思路

OOM是面试和实战都高发的问题,我把排查套路总结成四步。

第一步看日志。启动参数加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path,JVM会在OOM发生时自动导出堆转储文件。第二步看堆转储,用MAT或jvisualvm分析,找谁占用了最多的堆内存,搜索一个对象一个对象去查引用链。第三步看代码逻辑,常见OOM场景有:大对象列表没分页查询、ThreadLocal用完没remove导致线程池里的线程持有对象引用、字符串拼接产生大量中间对象、直接内存使用ByteBuffer分配过多。第四步看JVM参数,堆太小就调大,但要先确认是被业务对象占用还是泄漏,如果是正常对象多就得调大堆而不是优化代码。

很多人在面试里只说“用jmap导出dump分析”,不够。建议再补充:线上通常先执行jmap -histo:live看存活对象类型TOP10,快速定位大对象;然后用jstat -gcutil观察GC频率;最后再看dump。这三个命令一套组合拳下来,大部分OOM都能定位到根因。

4. 并发编程:从synchronized到JUC

4.1 synchronized的锁升级过程

并发是Java面试的分水岭,而synchronized是必考中的必考。很多人知道synchronized能保证原子性、可见性、有序性,但知其然不知其所以然。JDK6之前synchronized是重量级锁,直接依赖操作系统互斥量,线程竞争时会发生用户态和内核态切换,开销很大。JDK6做了锁升级优化,锁的状态从无锁到偏向锁到轻量级锁再到重量级锁。

锁升级的过程要能说清楚:刚开始一个线程访问同步块,锁偏向这个线程,记录线程ID,不触发额外的同步操作;如果有其他线程来竞争,偏向锁撤销,升级为轻量级锁,线程用CAS在对象头Mark Word里尝试替换锁记录指针,抢不到就自旋等待;自旋超过阈值(默认10次或自适应)仍然抢不到,就升级为重量级锁,后续竞争线程进入阻塞状态。

注意一个细节:偏向锁在JDK15被默认禁用,JDK17之后被标记为废弃并彻底移除。如果你面试时提到偏向锁,最好顺带说明这一点,否则面试官可能觉得你的知识停留在老版本。JDK8环境下偏向锁仍有意义,但新版本JDK(如JDK17、JDK21)已经不建议依赖偏向锁优化。

4.2 volatile的可见性与禁止重排

volatile是并发面试第二高频考点。核心作用两个:保证可见性,禁止指令重排序。可见性的底层实现是通过内存屏障和缓存一致性协议。写volatile变量时,JVM会在写操作后插入StoreStore屏障和StoreLoad屏障,强制把当前线程工作内存中的变量刷新到主内存;读volatile变量时,插入LoadLoad屏障和LoadStore屏障,把主内存的最新值读到工作内存。

第二个作用是禁止指令重排,典型场景是双重检查锁单例模式。不加volatile的单例在并发下可能拿到一个“半初始化”的对象,因为创建对象的过程分为分配内存、初始化字段、将引用赋值给变量三步,JVM可能重排成分配内存、引用赋值、初始化字段,另一个线程在读引用的时看到非null但对象还没初始化完,用起来就出问题了。加了volatile之后,写volatile变量之前的操作不能被重排到写之后,就能保证对象完全初始化后引用才可见。

面试官还喜欢问“volatile能保证原子性吗”。答案是不能。经典的i++问题,i++是三个原子操作组合:读i、i+1、写回i。volatile只保证读和写单步操作的可见性,但三步之间可能被其他线程插队。解决i++原子性问题要用AtomicInteger的CAS,或者在方法上加synchronized。

4.3 线程池参数与拒绝策略

线程池这块几乎是必考的。先说ThreadPoolExecutor的七个核心参数:核心线程数corePoolSize、最大线程数maximumPoolSize、空闲存活时间keepAliveTime、存活时间单位unit、任务队列workQueue、线程工厂threadFactory、拒绝策略handler。

参数怎么定是个经典追问。建议分场景回答:CPU密集型任务,核心线程数设为CPU核数+1;IO密集型任务,核心线程数设为CPU核数*2或更多,因为线程大部分时间在等待IO。这里的“+1”和“乘2”不是死规则,老项目里常用公式:CPU密集型用N+1,IO密集型用2N。另外要看队列类型:使用无界队列LinkedBlockingQueue时,maximumPoolSize参数基本失效,因为任务永远不会拒绝,极端情况队列堆积十几万个任务会吃掉大量内存;有界队列ArrayBlockingQueue则配合拒绝策略使用。

执行流程要背熟:新任务提交后,如果当前线程数小于corePoolSize,直接创建核心线程执行;线程数达到corePoolSize后,任务进入队列等待;队列也满了,创建非核心线程(线程总数不超过maximumPoolSize)执行;非核心线程也满了,执行拒绝策略。四种拒绝策略:AbortPolicy直接抛异常,CallerRunsPolicy让提交任务的线程自己执行,DiscardPolicy直接丢弃,DiscardOldestPolicy丢弃队列里最老的任务。

我实际使用中的一个心得是:线上生产环境不要用无界队列,更建议用有界队列+CallerRunsPolicy,既能削峰,又能在系统过载时反馈到调用方,让入口限流自然生效。还有一点,线程池里的线程要起名字,用ThreadFactory设置有业务含义的前缀,否则线上排查线程问题时,你根本分不清任务跑在哪个线程池里。

4.4 ReentrantLock与AQS核心原理

ReentrantLock常被拿来和synchronized做对比。区别要答四点:ReentrantLock需要手动加锁和解锁,而synchronized自动释放;ReentrantLock支持公平锁,synchronized只能是非公平;ReentrantLock提供了可中断获取锁lockInterruptibly,以及tryLock支持超时获取;ReentrantLock支持多个条件队列Condition,synchronized只有一个监视器条件。

AQS是AbstractQueuedSynchronizer的缩写,是很多JUC同步工具的基础。核心机制是:一个volatile int类型的state变量表示同步状态,一个CLH变体的双向队列保存等待线程。以ReentrantLock为例,线程获取锁时用CAS把state从0改成1,成功就持有锁;失败就封装成Node加入等待队列尾部,并自旋或阻塞;释放锁时把state减1,减到0表示完全释放,唤醒队列中第一个等待线程。

Condition的原理也值得一说:每个Condition有一个等待队列,await方法把当前线程封装成Node加入等待队列并释放锁,signal方法把等待队列中的节点转移到AQS同步队列,唤醒后竞争锁。

4.5 并发常见问题:死锁、ABA、伪共享

并发部分最后汇总几个高频问题。

死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。排查经验要答:先用jps找到Java进程id,再用jstack打印线程栈,查找“Found one Java-level deadlock”的提示,然后定位代码里两个线程各自持有哪些锁以及想要哪些锁。预防方案:加锁顺序一致、用tryLock超时、减少锁粒度。

ABA问题是CAS操作中的经典问题:一个线程读到的值是A,期间被其他线程改成B又改回A,这个线程执行CAS时发现值还是A,就认为没被修改过。解决方法是加版本号,Java里的AtomicStampedReference就是干这个的。

伪共享可能很多人只在八股里见过,但性能敏感项目里很常见。当多个线程修改同一缓存行(64字节)上的不同变量时,缓存一致性协议会让整行缓存失效,导致性能下降。解决办法有@Contended注解和缓存行填充。曾经有个线上性能案例,一个数组里多个线程各自修改不同槽位,QPS上不去,最终定位就是伪共享,对齐缓存行后性能提升了近三倍。

5. Java 8新特性:从Lambda到Stream

5.1 Lambda表达式的本质是什么

Lambda是Java 8最显眼的改动,但很多人只停留在“简化匿名内部类”的层面。面试能往上答一层的说法是:Lambda表达式的本质是一个函数式接口的实例,编译后通过invokedynamic指令创建,并不是简单的匿名内部类语法糖。

函数式接口是只包含一个抽象方法的接口,可以用@FunctionalInterface注解标识。Runnable、Comparator、Callable、Consumer、Function、Predicate、Supplier都是常用函数式接口。写Lambda时要注意捕获外部局部变量必须是final或等效final,这是为了在创建Lambda时把变量值拷贝到闭包环境中,确保线程安全性和一致性。

Java 8还在标准库中提供了丰富的方法引用语法:类名::静态方法、实例::实例方法、类名::实例方法、类名::new构造引用。方法引用不是性能优化点,而是让代码更符合声明式思维,有经验的面试官会期待你点出这一点。

5.2 Stream API:惰性求值与流水线

Stream是Java 8新特性里最能体现“人人都会用但用法五花八门”的部分。要答出Stream不是集合,而是数据流,不会存储数据,它提供的是对数据源的高阶抽象操作。

操作分两类:中间操作是惰性的,只有遇到终止操作才会真正执行。比如filter、map、sorted是中间操作,collect、forEach、count、reduce是终止操作。这个设计很重要,Stream可以无限追加中间操作而不会产生额外开销,遇到短路操作的limit时还能提前终止遍历,性能更好。

可能被追问的点是parallelStream。它底层用ForkJoinPool实现并行流处理,但并行不总是更快,线程切换开销和拆分成本可能超过收益。数据量小或操作简单时,串行流反而更快。在自定义使用parallelStream时,要特别注意线程池默认是公共的ForkJoinPool,所有parallelStream任务共享它,任何任务里的阻塞都可能拖垮其他任务。

5.3 Optional怎么用才不算滥用

Optional诞生的目的是解决NullPointerException,但实际代码里被大量错误使用。最典型的问题是还在用Optional.ofNullable(x).get()或者if(optional.isPresent()),这跟直接判空没有本质区别,只是把代码写得更啰嗦了。

更推荐用法是:用Optional做安全的函数式链式调用,比如optional.map(...).orElse(defaultValue)、optional.flatMap(...).orElseThrow(() -> new BusinessException("xxx"))。Optional适合做返回值包装,告诉调用方“这个值可能不存在,请自行决定兜底策略”,而不适合做方法参数类型,也不适合作为类的字段类型,因为Optional本身没有实现序列化,作为字段会破坏对象序列化兼容性。这个细节知道的人不多,面试中说出来会加分。

6. Spring与Spring Boot:框架八股的重头戏

6.1 IOC与AOP的核心思想

Spring框架是Java后端面试绕不开的板块,IOC和AOP必须讲透。IOC(控制反转)的核心是:对象不是由自己new出来,而是交给Spring容器创建和管理,业务代码只声明依赖关系,容器负责注入。这样做最大的价值是解耦,模块之间依赖接口而非具体实现,替换实现类时不需要改动调用方代码。

AOP(面向切面编程)的实现机制要答两层。底层动态代理有两种方式:JDK动态代理要求目标类实现接口,基于InvocationHandler和Proxy类生成目标接口的代理对象;CGLIB代理不要求接口,通过生成目标类的子类并重写方法实现增强。Spring Boot 2.x之后默认使用CGLIB代理,即使有接口也默认走CGLIB,除非显式配置proxyTargetClass=false。

AOP经典使用场景:事务管理、日志记录、权限校验、接口耗时统计、异常兜底。我的经验是AOP很适合做横向逻辑,但不要滥用,用AOP处理频繁读写的热点方法时,要留意代理调用开销和自调用失效问题。Spring AOP中的方法自调用不会走代理,比如一个类中的方法A调用同类的方法B,B上的@Transactional不会生效,因为调用发生在target内部而不是经过代理对象。解决方法是注入自身代理或者拆类。

6.2 Bean的生命周期

这个题常考,答得是否完整基本能看出有没有系统复习过。完整流程是:实例化、属性填充、初始化、使用、销毁。

细化版本:Spring扫描到BeanDefinition后通过反射创建实例;然后做属性填充,把@Autowired、@Resource等依赖注入进去;接着执行Aware接口回调,比如BeanNameAware、BeanFactoryAware、ApplicationContextAware;再执行BeanPostProcessor的postProcessBeforeInitialization方法;然后执行@PostConstruct标注方法和InitializingBean的afterPropertiesSet方法;再执行BeanPostProcessor的postProcessAfterInitialization方法,这一步是AOP代理生成的关键节点,AbstractAutoProxyCreator在这里为Bean创建代理对象;最后Bean放入单例池,开始使用。容器关闭时,执行@PreDestroy和DisposableBean的destroy方法。

常见追问:Spring如何解决循环依赖。答法是三级缓存机制:一级缓存是成品单例池singletonObjects,二级缓存是提前暴露的半成品对象earlySingletonObjects,三级缓存是ObjectFactory工厂缓存singletonFactories。Bean实例化但还没完成属性填充时,就把ObjectFactory放入三级缓存,A依赖B时先拿到A的引用(可能是半成品),B完成创建后再填充A的属性,这样解决了大部分setter注入的循环依赖。但构造器注入的循环依赖无法通过三级缓存解决,因为构造器注入在实例化阶段就需要依赖对象,这时候连半成品都没有,只能报错。

6.3 Spring Boot自动配置原理

Spring Boot和Spring的区别是必答题。Spring Boot没有替代Spring,它只是简化了Spring的配置和启动流程。核心思想是约定大于配置。

自动配置原理的答案要落到@SpringBootApplication这个注解上。它由三个注解组成:@SpringBootConfiguration标记为配置类,@ComponentScan扫描当前包和子包下的组件,@EnableAutoConfiguration是核心,导入AutoConfigurationImportSelector类,该类扫描META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(早期版本是spring.factories),拿到所有自动配置类名称,逐一加载,并通过@ConditionalOnClass、@ConditionalOnMissingBean等条件注解按需生效。

这里有个高频追问:如何自己写一个starter。回答思路:创建autoconfigure模块定义配置类和条件装配逻辑,创建starter模块引入autoconfigure依赖,在META-INF文件中注册自动配置类。然后在配置类里用@Bean配合@ConditionalOnMissingBean提供默认实现,业务项目中引入依赖后,Spring Boot就会自动装配。这个技能在工作里很实用,团队内部做通用组件时几乎都要用。

6.4 Spring事务与传播行为

事务这题需要注意通过AbstractPlatformTransactionManager到TransactionInterceptor的整个链路。简易答法:Spring事务通过AOP实现,内部调用事务管理器的getTransaction、commit、rollback,有异常时回滚。@Transactional默认只在RuntimeException和Error时回滚,检查异常不回滚,需要显式配置rollbackFor。

事务传播行为有七种,常考三种:REQUIRED(默认,有事务就加入,没有就新建)、REQUIRES_NEW(挂起当前事务,开启新事务)、NESTED(嵌套事务,Savepoint方式回滚)。实际场景题:方法A调用方法B,两个方法都标了REQUIRED,B抛异常后A的try-catch能把异常吞掉吗?答案是不能,REQUIRED传播下两个方法共享一个事务,B标记事务回滚后整个事务被标记rollback-only,A提交时仍会抛UnexpectedRollbackException。如果B的业务很重要但不想拖垮A,就把B改成REQUIRES_NEW,B的事务失败不影响外层事务。

自己实现API签名对接时还要注意@Transactional失效场景:方法非public、自调用、异常被catch吞掉、类没有被Spring管理、数据库引擎不支持事务(比如MyISAM)、多线程中数据源使用不当导致连接不是同一个。我在Code Review里最常看到的就是try-catch吞异常导致事务不回滚,这个问题面试里一定会考。

7. 数据库与Redis:后端面试的必考组合

7.1 索引结构与最左前缀原则

数据库题目要从基础索引开始答。InnoDB的索引结构是B+树,叶子节点存储数据行或主键值,非叶子节点只存索引键和子节点指针。为什么选B+树而不是B树或哈希:B+树的非叶子节点可以存更多索引项,树更矮,查询IO更少;叶子节点之间用双向链表串联,范围查询非常高效;数据都在叶子节点,查询性能稳定。

最左前缀匹配要能举例说明。联合索引(a,b,c),查询条件中用到a或者a,b或者a,b,c才能命中索引,跳过了a直接用b或c没法走该索引,因为B+树按联合索引的第一列从左到右排列。a,b但b是范围查询时,c的索引会部分失效(范围之后的列无法用到索引)。MySQL 8.0之后做了索引下推优化,对a,b条件下可以继续用c做过滤,减少回表。

7.2 事务隔离级别与MVCC

事务的ACID特性背完,重点答隔离级别。四种隔离级别从低到高:读未提交、读已提交、可重复读、串行化。MySQL InnoDB默认是可重复读。这里有个容易丢分的点:MySQL的可重复读通过MVCC解决了快照读的幻读,但当前读(比如select for update)在可重复读下仍可能出现幻读,真正彻底解决需要串行化或间隙锁。InnoDB加了next-key lock(行锁+间隙锁)来防止当前读的幻读,但也不是100%覆盖所有场景。

MVCC实现要能说清三个隐藏字段:DB_TRX_ID(最近修改事务ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(行ID)。读操作根据ReadView判断可见性,ReadView里记录了活跃事务列表,读时刻后的数据通过undo log回滚到可见版本。读已提交每次select都生成新的ReadView,可重复读只在第一次select生成ReadView,之后的select都复用同一个快照。

7.3 慢SQL排查与索引失效场景

慢SQL题目要结合实际。定位手段:开启慢查询日志记录执行时间超过阈值的SQL,用EXPLAIN查看执行计划,重点关注type列是否走全表扫描(ALL)、key列是否有可用的索引、rows列扫描行数是否过大。

索引失效的常见场景我已经在实际工作里踩过很多次:对索引列使用函数,比如where date(create_time)=...导致索引失效;隐式类型转换,字符串字段用数字查;前导模糊查询like '%abc';or连接的非索引列;联合索引不满足最左前缀;not in和!=有时不走索引。还要注意:即使索引列参与运算也会失效,比如where id+1=10就不如where id=9。

7.4 Redis缓存穿透、击穿、雪崩

缓存题必须结合场景答。缓存穿透指查询一个不存在的数据,请求直接打到数据库。解决办法:缓存空值(设置短过期时间)、布隆过滤器拦截、接口层参数校验。布隆过滤器的原理可以提一句:多个哈希函数映射到位数组,判断可能存在或一定不存在,有一定误判率但不存在的判断是确定的。

缓存击穿指缓存Key过期瞬间,大量并发请求同时打到数据库。解决办法:互斥锁重建缓存,只让一个线程查数据库,其他线程等待;或者热点数据设置为逻辑过期,后台异步更新。

缓存雪崩指大量Key同时过期或Redis宕机。解决办法:过期时间加随机值,避免同时过期;使用Redis集群保证高可用;构建多级缓存兜底。追问环节可能会问“Redis持久化方式”,这时候补上RDB和AOF区别:RDB是定时快照,恢复快但可能丢数据;AOF是追加日志,根据fsync策略决定数据丢失窗口;生产环境通常RDB+AOF混合使用。

8. 微服务与分布式:拉开差距的地方

8.1 CAP理论:分布式系统的基石

分布式必考CAP。一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),三者最多同时满足两个。实际分布式系统里,网络分区无法避免,所以P必须选,剩下在C和A之间抉择。

技术选型要对号入座:ZooKeeper保证CP,牺牲可用性,Master选举期间短暂不可用;Eureka保证AP,节点间数据可能不一致,但服务列表始终可读;Nacos可以切换CP和AP模式。这里面试官常追问“CP系统的注册中心在分布式环境下怎么做到一致”,你需要说清楚ZAB或Raft协议的Leader选举和日志复制过程,一句话即可:要么Leader写成功并同步到多数节点后返回,要么进入选举状态不可写入。

8.2 分布式事务:2PC、TCC与MQ最终一致性

微服务架构下分布式事务几乎是必问的。2PC两阶段提交:准备阶段所有参与者执行事务但不提交,协调者发提交命令后所有参与者提交;一旦有参与者失败,协调者发中断命令全部回滚。缺点是同步阻塞、协调者单点、第二阶段不能百分之百保证所有节点提交或回滚成功,所有参与者都持有资源,性能也不高。

TCC是补偿事务:Try阶段冻结资源,Confirm阶段完成业务操作,Cancel阶段释放资源。它比2PC灵活,但实现复杂,需要为每个操作写三个方法。实际用的多的是消息事务和本地消息表方案:本地消息表把业务操作和数据变更放同一个本地事务,消息表记录待发送消息,异步发送到MQ,消费方通过幂等消费完成最终一致性。面试时选这个方案讲,既有实现细节又有工程经验,比背2PC效果好。

8.3 消息队列如何保证可靠性

MQ可靠性要从三个环节答:生产端、Broker、消费端。生产端可靠投递,开启confirm机制,发送后异步等Broker确认,确认失败走重试;Broker端开启持久化,消息落盘更保险的是刷盘策略配置为同步刷盘;消费端消费成功后手动提交offset,处理失败重试或进入死信队列。

幂等消费是关键。消息可能重复投递,消费端必须实现幂等:用业务唯一ID查重、数据库唯一约束、Redis setnx + 过期时间。我在项目里用的方案是消息中带业务流水号,消费前set nx到Redis,成功就处理业务,失败说明已消费过,直接ack。这题在面试里一旦展开,非常考验实战经验。

9. 手撕代码与设计模式:面试现场的“实战题”

9.1 单例模式的五种写法

手撕代码环节,单例是出现频率最高的。最推荐的是饿汉式、静态内部类和枚举。写双重检查锁时,重点是volatile那行,并说清楚为什么要加volatile(防止指令重排导致半初始化对象逸出)。

public class Singleton { // 关键:volatile保证可见性和禁止重排 private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { // 第一次检查,避免无谓的加锁 if (instance == null) { synchronized (Singleton.class) { // 第二次检查,保证并发下只有一个实例 if (instance == null) { instance = new Singleton(); } } } return instance; } }

静态内部类的写法也推荐:利用类加载的线程安全机制,JVM保证静态内部类只会被加载一次,天然线程安全,且懒加载。枚举写法则利用枚举构造器私有且JVM保证实例唯一性,同时避免反序列化破坏单例。反序列化这个问题容易被忽略:普通单例类实现Serializable后,反序列化可能生成新实例,要加readResolve方法返回原实例。

9.2 排序算法:快速排序与冒泡排序

手撕算法考排序最常见的是快速排序和冒泡排序。快速排序要能写出来并分析复杂度:

public void quickSort(int[] arr, int left, int right) { if (left >= right) return; int pivot = partition(arr, left, right); quickSort(arr, left, pivot - 1); quickSort(arr, pivot + 1, right); } private int partition(int[] arr, int left, int right) { int pivot = arr[right]; int i = left - 1; for (int j = left; j < right; j++) { if (arr[j] < pivot) { i++; swap(arr, i, j); } } swap(arr, i + 1, right); return i + 1; }

快排平均时间复杂度O(nlogn),最坏O(n^2),最坏情况发生在每次分区都极端不平衡时(比如已排序数组配固定基准)。优化手段:随机选取基准点、三数取中法、小区间切换插入排序。冒泡排序考得少,但有时会被要求手写并说明优化方案:如果一轮比较下来没有发生交换,说明数组已有序,可以提前退出。少量数据用冒泡可以,大量数据还是快排或归并稳定。

9.3 常见设计模式考点

设计模式面试重点在单例、工厂、策略、观察者、模板方法、代理。工厂模式要分清简单工厂、工厂方法、抽象工厂;策略模式常结合业务场景问,比如支付渠道选择、优惠券计算;观察者模式对应Spring事件机制,@EventListener的实现原理是监听器注册和事件广播;模板方法模式在Spring里最常见的体现是JdbcTemplate和RestTemplate对流程的封装。

代理模式结合Spring AOP一起答,说JDK动态代理和CGLIB的区别及选择依据就够了。如果面试官跟你聊“策略模式怎么解决大量if-else”,你可以用枚举或Map(策略编号到实现类的映射)来实现,代码更整洁,扩展时只需新增策略类,不需要改动分发表。

10. Java环境与编译的冷门考点

最后再补一类容易被忽略的题。热搜词里频繁出现“java环境变量配置”、“源发行版17需要目标发行版17”、“Lombok报错”这类实际开发中的关键字,面试虽然不会直接问“怎么配环境变量”,但常以“你遇到过什么编译错误”的方式出现。

源发行版17需要目标发行版17这个报错的本质是:JDK版本和编译目标版本不一致。maven项目要在pom.xml里统一配置maven.compiler.source和maven.compiler.target,更推荐用properties里统一设置java.version。IDE里还要检查Project Structure的SDK版本和Language Level是否与pom一致。这个问题在多人协作项目频繁出现,新成员克隆代码后一编译就报,典型的“环境不一致”问题。

Lombok的报错“You aren't using a compiler supported by lombok”是说Lombok版本和JDK版本不匹配。Lombok通过注解处理器在编译期修改AST,新版本JDK改动内部API后,旧版Lombok就无法工作。解决方法是升到最新版Lombok,并检查IDE的注解处理是否开启。这个错误在升级JDK 17或JDK 21的项目里特别常见,面试里能准确说出原因和解决办法,说明你真的有线上经验。

至于vscode运行Java报错乱码,通常是控制台编码和文件编码不一致导致的,在launch.json里配置console编码或统一文件编码为UTF-8即可。这类问题虽然不起眼,但能反映候选人排错的基本功。

这份总结写到这,把Java面试最核心的知识点都串起来了。我个人最大的体会是:八股背得再熟,不如多问自己几个“为什么”,面试官真正在意的不是你能背出HashMap负载因子是0.75,而是你知不知道它为什么是0.75、并发下会出什么问题、线上遇到了怎么排查。准备面试的这段时间,建议每个章节都对照源码和线上排查工具过一遍,面试时你会明显感觉思路清晰很多。最后再说一句,面试是双向检验,别光背题,多动手写代码、多跑案例,你的表达深度自然就不一样了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 7:34:14

MLPerf 发布端到端 RAG 推理基准

MLCommons MLPerf Inference 工作组推出首个端到端检索增强生成&#xff08;RAG&#xff09;推理基准。通过在查询时从检索文档而非仅模型权重中生成答案&#xff0c;RAG 显著降低幻觉&#xff0c;同时利用最新私有知识&#xff0c;已成为语言模型最常见的部署方式之一。 RAG 生…

作者头像 李华
网站建设 2026/8/30 7:32:22

2016年360研发工程师笔试题复盘:考点、解题思路与备考策略

前几天整理移动硬盘&#xff0c;翻到自己当年备战360公司2016研发工程师笔试题时存的笔记和草稿&#xff0c;索性花了一晚上重新梳理了一遍。说实话&#xff0c;那个年代的笔试题放到今天来看&#xff0c;核心板块其实没怎么变——C/C、数据结构、操作系统、计算机网络这几座大…

作者头像 李华
网站建设 2026/8/30 7:31:28

模糊卡尔曼滤波在设备寿命预测中的协同建模方法

简介&#xff1a;本资源是一套面向机械故障诊断与预测性维护领域的MATLAB实践代码包&#xff0c;聚焦于融合模糊逻辑与卡尔曼滤波的剩余寿命预测方法&#xff0c;适用于具备基础信号处理与状态估计知识的研究生、工程师及可靠性分析从业者。压缩包共27个文件&#xff08;964KB&…

作者头像 李华
网站建设 2026/8/30 7:30:25

C#无人值守地磅系统开发实战:从串口通讯到状态机设计

简介&#xff1a;本资源是一套基于C#开发的汽车衡称重与无人值守地磅过磅系统完整源码&#xff0c;面向工业自动化软件开发者、智能物流系统集成工程师及智能制造领域技术学习者&#xff0c;解决传统地磅人工干预多、效率低、易出错等痛点&#xff0c;适用于物流园区、矿山、电…

作者头像 李华
网站建设 2026/8/30 7:29:27

AI漫剧制作全流程:从角色一致到批量生成实战指南

这次我们来看一个很有意思的 AI 漫剧案例&#xff1a;《穿成将军嫡女绑定废柴攻略系统&#xff0c;我索性直接摆烂躺平&#xff0c;系统惩罚全部转嫁到战神身上&#xff0c;高冷将军反倒开启疯狂自我攻略模式》。这个标题本身就是典型的网文爽感短剧梗&#xff0c;加上“AI漫剧…

作者头像 李华