
先坦白说这份2026版Java八股面试文是我把近几年面过的人、自己被问过的题、以及身边大厂朋友反馈的高频考点重新梳理后整理的。大而全的八股清单网上很多但真正带着答案、带着坑点、带着“为什么这么答”的版本很少所以这篇万字长文值得你收藏起来面试前翻一翻胜过临时抱佛脚。这篇文章覆盖Java基础、集合、JVM、并发、新特性、Spring、数据库、分布式、算法和设计模式每个题都按“怎么答为什么这么答追问点”的套路来写。不管你是刚准备找实习的在校生还是工作了几年想跳槽的Java工程师这篇都能直接当复习提纲用。文章很长建议先收藏再慢慢看。1. Java基础核心题面试官最爱的“送分题”与陷阱题1.1 JDK、JRE、JVM的区别一条必答题这道题看起来简单但每年都能刷掉一批人。标准答法是JDK是Java开发工具包包含编译器javac、调试器jdb等工具还包含JREJRE是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的实现抛弃了分段锁改为CASsynchronized锁桶头节点锁粒度更细并发度更高。补充一条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/pathJVM会在OOM发生时自动导出堆转储文件。第二步看堆转储用MAT或jvisualvm分析找谁占用了最多的堆内存搜索一个对象一个对象去查引用链。第三步看代码逻辑常见OOM场景有大对象列表没分页查询、ThreadLocal用完没remove导致线程池里的线程持有对象引用、字符串拼接产生大量中间对象、直接内存使用ByteBuffer分配过多。第四步看JVM参数堆太小就调大但要先确认是被业务对象占用还是泄漏如果是正常对象多就得调大堆而不是优化代码。很多人在面试里只说“用jmap导出dump分析”不够。建议再补充线上通常先执行jmap -histo:live看存活对象类型TOP10快速定位大对象然后用jstat -gcutil观察GC频率最后再看dump。这三个命令一套组合拳下来大部分OOM都能定位到根因。4. 并发编程从synchronized到JUC4.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、i1、写回i。volatile只保证读和写单步操作的可见性但三步之间可能被其他线程插队。解决i原子性问题要用AtomicInteger的CAS或者在方法上加synchronized。4.3 线程池参数与拒绝策略线程池这块几乎是必考的。先说ThreadPoolExecutor的七个核心参数核心线程数corePoolSize、最大线程数maximumPoolSize、空闲存活时间keepAliveTime、存活时间单位unit、任务队列workQueue、线程工厂threadFactory、拒绝策略handler。参数怎么定是个经典追问。建议分场景回答CPU密集型任务核心线程数设为CPU核数1IO密集型任务核心线程数设为CPU核数*2或更多因为线程大部分时间在等待IO。这里的“1”和“乘2”不是死规则老项目里常用公式CPU密集型用N1IO密集型用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支持多个条件队列Conditionsynchronized只有一个监视器条件。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到Stream5.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除非显式配置proxyTargetClassfalse。AOP经典使用场景事务管理、日志记录、权限校验、接口耗时统计、异常兜底。我的经验是AOP很适合做横向逻辑但不要滥用用AOP处理频繁读写的热点方法时要留意代理调用开销和自调用失效问题。Spring AOP中的方法自调用不会走代理比如一个类中的方法A调用同类的方法BB上的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两个方法都标了REQUIREDB抛异常后A的try-catch能把异常吞掉吗答案是不能REQUIRED传播下两个方法共享一个事务B标记事务回滚后整个事务被标记rollback-onlyA提交时仍会抛UnexpectedRollbackException。如果B的业务很重要但不想拖垮A就把B改成REQUIRES_NEWB的事务失败不影响外层事务。自己实现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 %abcor连接的非索引列联合索引不满足最左前缀not in和!有时不走索引。还要注意即使索引列参与运算也会失效比如where id110就不如where id9。7.4 Redis缓存穿透、击穿、雪崩缓存题必须结合场景答。缓存穿透指查询一个不存在的数据请求直接打到数据库。解决办法缓存空值设置短过期时间、布隆过滤器拦截、接口层参数校验。布隆过滤器的原理可以提一句多个哈希函数映射到位数组判断可能存在或一定不存在有一定误判率但不存在的判断是确定的。缓存击穿指缓存Key过期瞬间大量并发请求同时打到数据库。解决办法互斥锁重建缓存只让一个线程查数据库其他线程等待或者热点数据设置为逻辑过期后台异步更新。缓存雪崩指大量Key同时过期或Redis宕机。解决办法过期时间加随机值避免同时过期使用Redis集群保证高可用构建多级缓存兜底。追问环节可能会问“Redis持久化方式”这时候补上RDB和AOF区别RDB是定时快照恢复快但可能丢数据AOF是追加日志根据fsync策略决定数据丢失窗口生产环境通常RDBAOF混合使用。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 arent 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、并发下会出什么问题、线上遇到了怎么排查。准备面试的这段时间建议每个章节都对照源码和线上排查工具过一遍面试时你会明显感觉思路清晰很多。最后再说一句面试是双向检验别光背题多动手写代码、多跑案例你的表达深度自然就不一样了。