公司动态

Java面试八股文高效复习:从集合框架到JVM、并发、Spring与MySQL核心原理

📅 2026/8/30 6:58:48
Java面试八股文高效复习:从集合框架到JVM、并发、Spring与MySQL核心原理
Java 面试八股文值得背吗我的答案是值得但千万别死记硬背。做了这么多年 Java 开发也面试过不少人我发现很多候选人把八股文背得滚瓜烂熟结果一问到底层原理就露馅。反过来也有人嗤之以鼻觉得八股文没用结果面试时连HashMap的扩容机制都说不出个所以然。八股文这东西本质上是前人把高频考点、核心原理总结出来的知识清单它帮你划重点、理脉络但前提是你真的理解了背后的原理而不是把它当成背诵材料。这篇文章我会把 Java 面试里最常考的几大板块——集合、JVM、并发、Spring、MySQL——掰开揉碎讲一遍结合我自己的实操经验和踩过的坑帮你把八股文变成真正的内功。1. Java 基础高频考点集合框架与面向对象1.1 HashMap 的底层原理从数组到红黑树的进化HashMap几乎是 Java 面试必问的第一道题而且问法越来越细。我面试别人的时候喜欢从你用过 HashMap 吗开始一路追问到扩容、哈希冲突、红黑树化基本上能筛掉 80% 的人。先理清楚它的核心结构。HashMap底层是一个数组加链表的结构数组的每个位置叫桶bucket当多个 key 的哈希值落到同一个桶时就用链表把它们串起来。JDK 1.8 之后当链表长度超过 8 且数组长度大于等于 64 时链表会转成红黑树目的是把查询时间复杂度从 O(n) 降到 O(log n)。为什么阈值是 8源码注释里给过解释泊松分布下链表长度达到 8 的概率已经极低约千万分之六所以 8 是时间和空间的平衡点。很多人对扩容机制一知半解。HashMap默认初始容量是 16负载因子是 0.75意思是当元素个数超过容量 * 负载因子 12时就会触发扩容容量翻倍到 32。扩容时元素要重新计算哈希并分配到新桶里这就是为什么说HashMap的 put 操作在极端情况下代价很高。至于为什么负载因子是 0.75这其实是空间和时间的一个折中——太大比如 1会减少扩容次数但增加哈希冲突太小比如 0.5则浪费空间。再讲一个容易被忽略的细节HashMap的 key 为什么要求重写equals()时也要重写hashCode()因为查找时会先用hashCode()定位桶再用equals()比较链表中的元素。如果两个对象equals()相等但hashCode()不同它们会落到不同的桶里导致明明相等的 key 却被视为不同。我踩过这个坑用自定义对象做 key 时只重写了equals()没重写hashCode()结果 get 出来是 null排查了半天。提示JDK 1.8 中链表插入采用尾插法而 1.7 是头插法。头插法在多线程扩容时可能形成环形链表导致死循环。虽然现在大家基本都用 1.8但面试问到为什么 1.8 要改成尾插法你要能答上来。1.2 ConcurrentHashMap 如何保证线程安全HashMap在多线程环境下不安全这个大家都知道。但面试官往往不会只满足于用 ConcurrentHashMap而是会追问它到底怎么保证线程安全。这里有个分水岭JDK 1.7 和 1.8 的实现完全不同。1.7 的ConcurrentHashMap采用分段锁设计内部维护一个 Segment 数组每个 Segment 继承自ReentrantLock默认分成 16 段。写操作只需锁住对应的 Segment不同 Segment 之间互不影响所以并发度就是 16。但缺点是定位元素需要两次哈希先定位 Segment再定位桶。1.8 则抛弃了分段锁直接用CAS synchronized保证线程安全。put 时如果桶为空就用 CAS 直接插入避免加锁开销如果桶不为空就用synchronized锁住桶的头节点。锁粒度从 Segment 级细化到了桶级并发度大大提高而且synchronized经过 JVM 的锁升级优化后性能并不比ReentrantLock差。同时1.8 的ConcurrentHashMap也引入了红黑树优化和HashMap保持一致。面试时如果能把 1.7 和 1.8 的对比说清楚再补一句1.8 为什么敢用 synchronized——因为锁升级机制让无竞争场景下的 synchronized 几乎没有开销基本上这道题就稳了。1.3 ArrayList 与 LinkedList 的适用场景这道题看似简单但很多人答不到点子上。ArrayList底层是动态数组LinkedList底层是双向链表。数组支持随机访问所以get(index)是 O(1)链表需要从头遍历是 O(n)。但插入和删除呢很多人直接说链表快这是不对的。ArrayList在尾部插入是 O(1)但如果触发了扩容会涉及Arrays.copyOf复制整个数组摊还下来还是 O(1)。在中间插入则需要移动后续所有元素是 O(n)。LinkedList在头部插入确实快是 O(1)但如果要在中间插入你得先找到那个位置这个查找本身已经是 O(n) 了。所以在实际业务中LinkedList的优势并没有想象中那么大。我自己的经验是90% 的场景用ArrayList就够了LinkedList真正适合的是需要频繁在头部插入删除的场景比如实现一个队列。还有一个细节面试官爱问ArrayList默认容量是 10每次扩容是原来的 1.5 倍oldCapacity (oldCapacity 1)。如果你提前知道元素数量最好通过构造函数指定初始容量避免频繁扩容带来的性能损耗。这个在刷 LeetCode 或者处理大量数据时特别明显。2. JVM 核心知识从内存区域到垃圾回收2.1 运行时数据区域与对象创建流程JVM 这一块是 Java 面试的深水区也是区分会用 Java和懂 Java的分水岭。先讲运行时数据区域JDK 1.8 之后内存区域分为线程私有的虚拟机栈、本地方法栈、程序计数器和线程共享的堆、方法区/元空间。这里最容易被问到的就是栈和堆的区别以及方法区里存的是什么。方法区在 JDK 1.8 里被移到了元空间Metaspace使用的是本地内存而不是 JVM 堆内存。为什么要改因为永久代的大小很难确定容易出现OutOfMemoryError: PermGen space而元空间默认不受 JVM 内存限制只受系统可用内存限制大大减少了这类 OOM。对象创建的完整流程是类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行init方法。这里面试官喜欢追问如何判断对象可以被回收。目前主流是可达性分析算法从 GC Roots虚拟机栈引用的对象、静态变量引用的对象、常量引用的对象等出发凡是不可达的对象都标记为可回收。注意finalize()方法在 JDK 9 已经被标记废弃了原因很简单它的执行时机不确定而且可能被滥用容易造成内存泄漏。2.2 垃圾收集器选型CMS 与 G1 的取舍垃圾收集器是 JVM 面试的重头戏。常见的收集器有 Serial、Parallel、CMS、G1还有一个 JDK 11 引入的 ZGC。我建议你重点掌握 CMS 和 G1 的区别因为这是目前生产环境最常用的方案。CMSConcurrent Mark Sweep的设计目标是最短停顿时间适合对延迟敏感的应用。它的工作流程分四步初始标记STW、并发标记、重新标记STW、并发清理。其中只有初始标记和重新标记会停顿用户线程这两步耗时很短。但 CMS 有个著名的缺点内存碎片化。因为它用的是标记-清除算法不压缩空间长时间运行后会产生大量碎片可能导致Concurrent Mode Failure这时候会退化为 Serial Old 收集器做 Full GC停顿时间反而变长。G1 的设计理念是分区 可预测停顿。它将堆划分为多个大小相等的 Region每个 Region 可以独立回收通过维护一个优先列表每次都回收垃圾最多的 RegionGarbage First。G1 可以指定停顿时间目标-XX:MaxGCPauseMillis200它会根据这个目标动态调整回收策略。相比 CMSG1 最大的优势是解决了内存碎片问题因为它在回收后会做空间整理。提示如果你在生产环境还在用 JDK 8默认收集器是 Parallel Scavenge Parallel Old它追求的是高吞吐量而不是低延迟。如果你的业务对响应时间非常敏感可以考虑换成 G1但最好先在测试环境压测不要贸然上线改 GC 配置。2.3 内存泄漏排查实战MAT 与 jstack八股文背得再熟不如一次真实的内存泄漏排查经历有说服力。我在之前一个项目里遇到过一次典型的内存泄漏服务运行几天后老年代持续增长最终触发 Full GC接口响应从几十毫秒飙升到几秒。用jstat -gcutil pid可以看到老年代O使用率一直在 99% 徘徊Full GC 次数不断增加。排查步骤我建议按这个顺序来先用jps找到进程 PID再用jstat观察 GC 情况然后用jmap -dump:formatb,fileheap.bin pid抓堆转储文件最后用 Eclipse MAT 分析。MAT 打开后重点看 Leak Suspects 报告它会自动帮你定位到哪个对象占用了大量内存。我那次查下来罪魁祸首是一个静态 Map 存了用户的查询条件只往里放从没清理过日积月累就成了内存炸弹。如果怀疑是线程死锁或者线程阻塞用jstack pid导出线程栈查找deadlock关键字。排查 CPU 飙高的问题可以用top -Hp pid找出占用 CPU 最高的线程 ID转成十六进制后在jstack输出里搜索。这一套组合拳打过几次你对 JVM 的理解会远超背八股文的层次。3. 并发编程从 JMM 到锁优化3.1 volatile 与 JMM 的可见性保证并发编程是 Java 面试的第二个深水区。volatile是最基础的考点但也是最容易被问倒的。很多人只知道volatile 保证可见性不保证原子性但你要能说清楚它为什么能保证可见性。Java 内存模型JMM规定线程操作变量时会先从主内存把变量拷贝到自己的工作内存线程栈操作完再写回主内存。如果没有同步机制一个线程修改了变量另一个线程可能读到的还是自己工作内存里的旧值。volatile做了两件事写变量时JVM 会在写操作后插入一个内存屏障强制把工作内存中的修改刷新到主内存读变量时插入读屏障强制从主内存重新加载。同时它还会禁止指令重排序这就是 DCL 单例双重检查锁需要用volatile修饰instance的原因——防止new操作的指令重排导致其他线程拿到未初始化完成的对象。但volatile不保证原子性。经典的count问题即使把count声明成volatile多线程下依然会丢数据因为count是读-改-写三步操作volatile只保证了第一步读和最后一步写对其他线程可见中间的修改过程不可分割。要保证原子性得用AtomicInteger或者synchronized。3.2 synchronized 锁升级与 AQS 原理synchronized在 JDK 1.6 之后做了大量优化引入了偏向锁、轻量级锁、重量级锁的升级机制。很多人面试时只知道有锁升级这回事但说不清触发条件。偏向锁的想法是大部分情况下锁不仅不竞争而且总是由同一个线程获得。所以首次获取锁时会在对象头里记录线程 ID之后这个线程再进来不需要任何 CAS 操作。一旦有另一个线程来竞争偏向锁撤销升级为轻量级锁。轻量级锁通过 CAS 在对象头上抢锁如果抢不到或者竞争加剧就会继续膨胀为重量级锁依赖操作系统的互斥量实现涉及用户态到内核态的切换开销最大。AQSAbstractQueuedSynchronizer是ReentrantLock、Semaphore、CountDownLatch等同步工具的共同基石。它的核心是一个 volatile 的 state 变量加一个 FIFO 双向队列。以ReentrantLock为例线程尝试获取锁时会通过 CAS 把 state 从 0 改成 1成功则持有锁失败则封装成 Node 节点加入等待队列并LockSupport.park()挂起自己。释放锁时把 state 减回 0唤醒队头节点。理解 AQS 之后你会发现各种并发工具的本质都是对 state 的争夺 队列的管理。3.3 线程池参数设置与拒绝策略线程池是并发编程里实战性最强的考点。ThreadPoolExecutor有七个参数核心的是核心线程数corePoolSize、最大线程数maximumPoolSize、阻塞队列workQueue和拒绝策略handler。线程池的执行流程是当提交任务时如果当前线程数小于 corePoolSize创建新线程执行如果达到 corePoolSize任务进入阻塞队列如果队列也满了且线程数小于 maximumPoolSize创建临时线程如果线程数已经达到最大值触发拒绝策略。这里有一个容易被忽略的细节corePoolSize和maximumPoolSize之间的临时线程空闲超过keepAliveTime会被回收但核心线程默认不会。拒绝策略有四种AbortPolicy默认直接抛异常、CallerRunsPolicy由提交任务的线程自己执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列里最老的任务。我个人的建议是业务场景下不要用默认的AbortPolicy因为抛出异常可能导致调用方感知不到任务失败CallerRunsPolicy在多数场景下更合适它把压力反馈给调用方起到一种天然的背压效果。关于线程池大小怎么定有个经验公式CPU 密集型任务线程数设为CPU 核数 1IO 密集型任务线程数设为CPU 核数 * 2或者按CPU 核数 / (1 - 阻塞系数)计算阻塞系数一般为 0.8~0.9。但这是理论值实际得靠压测调优。我一般先在测试环境用 JMeter 或 wrk 做压测观察线程池队列积压情况和响应时间再逐步调整。注意Executors.newFixedThreadPool()和newCachedThreadPool()在生产环境不建议直接使用。前者用的是无界队列LinkedBlockingQueue任务堆积过多时可能 OOM后者的最大线程数是Integer.MAX_VALUE线程创建过多也可能 OOM。面试问到为什么不推荐 Executors这是加分项。4. Spring 核心IOC 与 AOP 的面试角度4.1 Bean 的生命周期与循环依赖Spring 是 Java 后端面试的必考框架而 Bean 的生命周期和循环依赖是两道经典题。Bean 的完整生命周期大致是实例化 → 属性填充 → Aware 接口回调如 BeanNameAware、ApplicationContextAware→ BeanPostProcessor 的 postProcessBeforeInitialization → 初始化方法PostConstruct、InitializingBean、自定义 init-method→ BeanPostProcessor 的 postProcessAfterInitialization → 使用 → 销毁。面试时不需要每个节点都背但至少要把实例化、属性填充、初始化、销毁这条主线说清楚并指出PostConstruct、InitializingBean、init-method的执行顺序。循环依赖是 Spring 面试的高频难点。场景是A 依赖 BB 依赖 A两者互相引用。Spring 解决循环依赖的核心机制是三级缓存一级缓存singletonObjects存放完整的单例 Bean。二级缓存earlySingletonObjects存放半成品的 Bean已实例化但未完成属性填充。三级缓存singletonFactories存放 Bean 的工厂方法用于提前生成代理对象。当 A 实例化后发现自己依赖 B就把 A 的工厂方法放入三级缓存然后去创建 B。B 实例化后依赖 A此时从三级缓存中找到 A 的工厂生成 A 的半成品放到二级缓存。B 完成属性填充后A 再从缓存中拿到 B 并完成自己的初始化。整个过程的核心就是先将未装配完的 Bean 暴露出去供其他 Bean 引用。但要注意Spring 只能解决单例 属性注入的循环依赖构造器注入的循环依赖是解决不了的因为构造器阶段 Bean 还没实例化出来无法提前暴露。这也是面试官喜欢追问的点为什么构造器注入会有这个问题。4.2 AOP 的底层实现与失效场景AOP面向切面编程的底层依赖动态代理。Spring Boot 2.x 默认使用 CGLIB 代理而 Spring Boot 1.x 默认是 JDK 动态代理。两者区别是JDK 动态代理要求目标类实现接口基于ProxyInvocationHandlerCGLIB 则通过继承目标类生成子类来代理不要求实现接口。JDK 1.8 之后 JDK 动态代理的性能已经不输 CGLIB但 Spring Boot 2.x 依然把 CGLIB 作为默认主要是为了避免某些场景下强制要求实现接口的麻烦。AOP 最常见的失效场景——同一个类内部方法自调用。比如一个 Service 的methodA()调用了同类里的Transactional方法methodB()事务是不生效的。原因很简单代理对象在外部调用时才生效而this.methodB()是直接调用原始对象的方法根本没经过代理。解决办法是注入自身的代理对象或者把methodB()拆分到另一个 Service。这个问题我在实际项目里踩过好几次坑代码写得看起来没问题但事务就是不回滚。4.3 Spring Boot 自动配置原理Spring Boot 之所以能开箱即用全靠自动配置。很多人写了好几年 Spring Boot 项目却说不清SpringBootApplication这个注解到底干了什么。它其实是一个组合注解SpringBootConfiguration表明这是个配置类、EnableAutoConfiguration开启自动配置、ComponentScan扫描当前包及子包下的组件。核心在EnableAutoConfiguration它通过Import(AutoConfigurationImportSelector.class)加载META-INF/spring.factories或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里声明的所有自动配置类。每个自动配置类通常带有ConditionalOnClass、ConditionalOnMissingBean等条件注解意思是只有当你引入了相关依赖且没有手动配置 Bean 时我才生效。这就是为什么你引入spring-boot-starter-data-redis后Spring Boot 会自动帮你创建RedisTemplate而你什么都不用配。面试时如果能顺便提一句自定义 Starter 的方法——写一个自动配置类在META-INF/spring/...AutoConfiguration.imports里声明再配合ConditionalOnMissingBean保证用户自定义优先——基本上就能证明你不只是会用 Spring Boot而是理解它的设计思想。5. MySQL 面试重点索引与事务5.1 索引的数据结构与最左前缀原则MySQL 的索引是后端面试绕不开的话题。InnoDB 引擎的索引底层是 B 树这里一定要理解 B 树和 B 树的区别B 树的非叶子节点不存数据只存索引值因此单节点能容纳更多索引树更矮磁盘 IO 次数更少而且 B 树的叶子节点用双向链表串起来非常适合范围查询。聚簇索引主键索引的叶子节点直接存储整行数据而二级索引辅助索引的叶子节点存的是主键值。这就是为什么回表——通过二级索引查数据时要先在二级索引的 B 树上找到主键再回聚簇索引查完整行数据。如果查询的字段恰好都包含在二级索引里就发生了覆盖索引不需要回表性能会好很多。最左前缀原则是面试必考。联合索引(a, b, c)实际上建立了a、a,b、a,b,c三个索引。查询条件如果只包含b和c是无法用到这个索引的因为必须从最左边的列开始匹配。但要注意MySQL 8.0 为了解决跳过字段的问题引入了索引跳跃扫描优化在某些条件下(b, c)查询也能用到索引但性能远不如完整前缀匹配不要依赖它。提示索引不是越多越好。每个索引都会占用额外的磁盘空间并降低写入性能因为每次 INSERT/UPDATE 都要维护索引树。我见过有些项目为了优化查询建了七八个索引结果写入慢得离谱。常规做法是只有热点查询路径、区分度高的字段才值得建索引。5.2 事务隔离级别与 MVCC事务的四个特性ACID和隔离级别属于送分题但 MVCC多版本并发控制才是拉开差距的地方。InnoDB 的默认隔离级别是REPEATABLE READ它通过 MVCC 解决了快照读的幻读问题当前读的幻读依然存在要靠间隙锁解决。MVCC 的核心是隐藏字段每一行数据都有隐藏的trx_id事务 ID和roll_pointer回滚指针。事务读取数据时会生成一个read view里面记录了当前活跃事务的 ID 列表。判断一条记录是否对当前事务可见的规则是如果记录的trx_id小于read view的最小活跃事务 ID说明是已提交事务可见如果大于最大活跃事务 ID不可见如果落在中间则判断是否在活跃列表里。通过这个机制REPEATABLE READ下事务第一次查询生成的read view在整个事务期间复用所以多次查询结果一致这就是快照读的实现原理。而READ COMMITTED每次查询都会生成新的read view所以能看到其他事务新提交的数据。5.3 慢 SQL 排查与优化思路慢 SQL 排查是实战性最强的内容。先开启慢查询日志SET GLOBAL slow_query_log ON;设置阈值long_query_time 1单位秒。定位到慢 SQL 后用EXPLAIN看执行计划重点看几个字段type访问类型从高到低依次是 system const eq_ref ref range index ALLALL 代表全表扫描必须优化、key实际用到的索引、rows预估扫描行数、Extra是否出现Using filesort、Using temporary这两个是要尽量避免的。我优化过一个典型的慢查询是一条关联查询EXPLAIN显示type是 ALL扫描了上百万行。原因是关联字段的字符集不一致导致索引失效。这里有个很容易踩的坑两个表字段都是varchar但一个表的字符集是utf8mb4另一个是utf8MySQL 在关联时无法直接使用索引要做隐式类型转换。解决办法是统一字符集。另一个常见优化点是避免SELECT *只查询需要的字段一方面减少网络传输另一方面可以让优化器更容易走覆盖索引。6. 面试实战技巧与复习建议6.1 如何把八股文变成自己的知识八股文最大的问题不是背而是只背不理解。我见过太多候选人对HashMap的源码参数倒背如流但问到他实际项目里怎么选型、遇到 OOM 怎么排查就支支吾吾。面试官真正想考察的不是你的记忆力而是你对原理的理解深度和解决问题的能力。我建议的复习方法是三步走第一步通读一遍主流八股文整理比如各类 Java 面试题汇总把知识点脉络理出来第二步每个知识点对应去读源码或官方文档比如HashMap的源码、ThreadPoolExecutor的源码不用全读读关键的 put、get、execute 方法就够了第三步把知识点映射到你自己的项目里想想哪里用到了这个机制、踩过什么坑、怎么解决的。完成这三步之后同一个知识点你回答的深度和厚度会完全不同。6.2 面试中加分的回答方式面试回答技术问题时有一个好用的框架先讲结论再讲原理最后结合场景。比如面试官问HashMap 线程安全吗你可以这么回答不安全。多线程同时 put 时可能导致数据覆盖JDK 1.7 扩容时还可能形成环形链表1.8 虽然改成了尾插法避免了死循环问题但数据丢失依然存在。如果线程安全需求建议用 ConcurrentHashMap它通过 CAS 和 synchronized 锁桶的方式保证了线程安全。这样回答既有结论又有对比还带出了 ConcurrentHashMap 的延伸知识点。另外一个加分技巧是主动暴露自己的知识边界。遇到不会的题不要硬编可以坦诚说这块我没有深入用过但我猜测大概是……——面试官往往更看重你思考问题的逻辑。我在面试中遇到候选人说这个我不太确定但根据我对 XX 原理的理解应该是……我反而会多聊几句如果候选人强行圆谎基本就直接扣分了。6.3 八股文的复习优先级如果时间紧张我给一个复习优先级参考。Java 基础集合、异常、泛型是必背概率最高JVM 内存模型和垃圾回收是第二梯队尤其是HashMap、ConcurrentHashMap、类加载机制、G1 收集器并发编程的synchronized、volatile、线程池是第三梯队这几块在高级岗位面试中出现频率极高Spring 和 MySQL 视岗位方向而定但大厂基本都会问。我个人体会是与其面面俱到不如把两三块内容学透。我见过一些候选人简历上写精通 JVM结果连-Xmx和-Xms的区别都说不清楚这种人反而是减分的。反过来你把 JVM 这块真正吃透能结合项目讲清楚内存调优的实践即使别的板块稍弱面试官也会觉得你有潜力。最后再分享一个小技巧准备面试时每学一个知识点试着用一句话向一个完全不懂 Java 的人解释清楚。如果解释不清楚说明你自己也没理解透。面试的本质不是背答案而是展示你在真实项目中解决问题的能力和思考深度八股文只是敲门砖内功才是真正的底气。