乐于分享
好东西不私藏

2026 Java面试进阶指南:源码级深挖38问,面试官最爱的追问都在这

2026 Java面试进阶指南:源码级深挖38问,面试官最爱的追问都在这
上一篇我们把八股文和AI新考点过了一遍,这篇换个姿势——不背答案,追源码、讲底层。面试官一旦往深了问,答案全在这篇文章里。
收藏,面试前照着这个目录过一遍。

一、集合源码:HashMap 全家桶(90%的人栽在这)

1. HashMap 底层结构到底怎么变?

JDK 1.7:数组 + 单向链表JDK 1.8:数组 + 链表 + 红黑树
为什么 1.8 要引入红黑树?——防退化。如果链表过长,查找从 O(1) 退化成 O(n),极端情况被恶意构造 Hash 碰撞直接 DoS。链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转红黑树,查找降回 O(log n)。
注意两个隐藏坑:
链表转树是 ≥ 8,树退化为链表是 ≤ 6(不是同一个阈值,中间留了缓冲带,防止反复横跳)
数组长度 < 64 时即使链表到 8 也不转树,而是优先扩容,因为扩容能直接打散冲突

2. HashMap 的 put 流程(源码背不背,区别很大)

  1. 对 key 求 hash((h = key.hashCode()) ^ (h >>> 16),高低 16 位异或,让高位也参与运算)
  2. 判断数组是否为空,为空先初始化(懒加载)
  3. 用 (n - 1) & hash 定位桶下标(等价取模,但位运算更快,前提 n 是 2 的幂)
  4. 桶为空直接放;不为空则遍历链表/树,key 相等覆盖,否则尾插
  5. 插入后判断是否超阈值,超了就扩容

3. HashMap 扩容机制 + 1.7 的"死循环"元凶

默认容量 16,负载因子 0.75,扩容为原来的2 倍(保持 2 的幂,位运算定位才能成立)。
1.7 死循环的根源:扩容时链表用的是头插法,多线程并发 rehash 时,两个线程互相把节点指来指去,形成环形链表,get 时死循环。1.8 改成尾插法 + 高位/低位拆分,根治了这个 bug——但注意,1.8 依然线程不安全,只是不再死循环,多线程 put 仍可能数据覆盖丢失。

4. HashMap / HashTable / ConcurrentHashMap 线程安全对比

容器
线程安全方案
锁粒度
效率
HashMap
HashTable
全表 synchronized
整个表
极低
ConcurrentHashMap(1.8)
CAS + synchronized
单个桶(Node)
面试官追问"ConcurrentHashMap 的 get 为什么不加锁"?——因为 Node 的 val 和 next 都用 volatile 修饰,读操作天然可见,无需加锁。

二、Java 基础进阶:问不倒系列

5. 深拷贝 vs 浅拷贝

浅拷贝:拷贝引用,两个对象指向同一份堆内存,一个改了另一个也变
深拷贝:对象本身 + 所有引用对象全部复制一份,互不影响
实现深拷贝三件套:实现 Cloneable 重写 clone()(要逐层手动 clone)、序列化反序列化(Serializable)、手动 new。序列化方案最省心但性能差,别在生产高频路径用。

6. 异常体系,一次讲透

Throwable├── Error(不可恢复,如 OOM、StackOverflowError,不该 catch└── Exception    ├── RuntimeException(unchecked,可不用显式处理,如 NPE、越界、ClassCastException)    └── Checked Exception(checked,编译期强制处理,如 IOException、SQLException)
面试送命题:finally 一定会执行吗?不一定——System.exit()、JVM 崩溃、守护线程终止时不会执行。还有 return 和 finally 的先后:try 里 return 的值会先缓存,finally 再执行,但 finally 里 return 会覆盖 try 的返回值。

7. 反射是什么?慢在哪?

运行时动态获取类信息、创建对象、调用方法。三入口:Class.forName()、对象.getClass()、类.class。
为什么慢?——每次调用要做权限检查、方法查找、装箱拆箱,还绕过了 JIT 的很多优化。Spring IOC、动态代理、注解解析全靠它。

8. 动态代理:JDK vs CGLIB

JDK 动态代理:基于接口,Proxy.newProxyInstance() + InvocationHandler,被代理类必须实现接口
CGLIB:基于继承,字节码生成子类,被代理类无需接口,但 final 类/方法不能代理
Spring AOP 默认:有接口走 JDK 代理,无接口走 CGLIB。

9. Java 8 新特性,面试必问三件

Stream:函数式操作集合,惰性求值,map/filter/collect 链式处理,注意并行流 parallelStream() 慎用(小数据量反而慢、有共享变量线程安全问题)
Optional:优雅处理 null,ofNullable/orElse/orElseThrow,避免 NPE 连环判空
函数式接口 + Lambda:@FunctionalInterface 只有一个抽象方法,Supplier/Consumer/Function/Predicate 四大金刚

三、JVM 进阶:源码级深挖

10. 类加载机制 + 双亲委派模型

加载阶段流程:加载 → 连接(验证 → 准备 → 解析)→ 初始化
双亲委派:AppClassLoader → ExtClassLoader → BootstrapClassLoader,一个类先交给父加载器加载,父加载不了再自己加载。
为什么要双亲委派?两个目的:①避免类被重复加载;②保证核心类库(如 java.lang.String)不被篡改——否则自己写个同名 String 类就能劫持系统。
追问:什么时候破坏双亲委派?——SPI(JDBC 的 DriverManager)、Tomcat 的多 Web 应用隔离、热部署(OSGi),这些场景需要子加载器优先加载。

11. JVM 内存区域划分

区域
线程
内容
异常
程序计数器
私有
当前执行的字节码行号
虚拟机栈
私有
方法调用的栈帧(局部变量表等)
StackOverflowError
本地方法栈
私有
native 方法
同上
共享
对象实例(新生代/老年代)
OOM
方法区/元空间
共享
类信息、常量、静态变量
OOM
注意:方法区 JDK 8 之前叫永久代,8 之后移到本地内存叫元空间(Metaspace),好处是默认不设上限、用本地内存,避免了永久代 OOM。

12. 对象创建过程

new → 检查类是否加载 → 分配内存(指针碰撞or空闲列表,取决于垃圾收集器是否规整)→ 初始化零值 → 设置对象头(Mark Word、类型指针)→ 执行  构造方法。
并发分配内存的解决方案:CAS 重试 + 本地线程分配缓冲(TLAB)

13. OOM 怎么排查?

  1. jmap -dump:format=b,file=heap.hprof
  2. 导出堆快照
  3. MAT / VisualVM 分析大对象、支配树、GC Roots 引用链
  4. jstat -gcutil
  5. 看 GC 频率和堆占用趋势
  6. jstack
  7. 看线程状态(排查死锁、线程堆积)
  8. 结合 -XX:+HeapDumpOnOutOfMemoryError 让 OOM 时自动 dump

四、并发进阶:从 volatile 到线程池

14. volatile 三大特性,缺一不可

可见性:写立即回写主存 + 缓存一致性协议(MESI)+ 内存屏障,读强制从主存拿
有序性:禁止指令重排(通过内存屏障)
不保证原子性:i++ 这种读-改-写复合操作照样出错,要原子性请用 AtomicInteger 或 synchronized
经典应用:双重检查锁(DCL)单例的volatile,防止指令重排导致拿到"半初始化"对象。

15. CAS 原理 + ABA 问题

compareAndSwap,比较并交换,原子类的底层(Unsafe 类提供)。CPU 指令级原子操作,无锁但会自旋,高并发下 CPU 开销大。
ABA 问题:值从 A 变 B 又变回 A,CAS 以为没变。解决:加版本号,AtomicStampedReference 或 AtomicMarkableReference。

16. 线程池七大参数 + 执行流程

核心参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
执行流程(背这个顺序):
线程数 < corePoolSize → 直接建核心线程
核心线程满了 → 入队(workQueue)
队列也满了 → 建非核心线程,直到 maximumPoolSize
全满了 → 触发拒绝策略
四种拒绝策略:AbortPolicy(抛异常,默认)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃最老任务)。
追问"为什么不用 Executors 创建线程池"?——newFixedThreadPool/newCachedThreadPool 用的队列要么无界要么线程数无界,极端情况 OOM,阿里的规范是手动new ThreadPoolExecutor

17. 线程的生命周期

NEW → RUNNABLE(就绪+运行)→ BLOCKED(锁阻塞)→ WAITING(无限等待)→ TIMED_WAITING(超时等待)→ TERMINATED。
高频混淆点:sleep() 不释放锁(抱着锁睡觉),wait() 释放锁(要在 synchronized 里用);wait/notify 是 Object 的方法,sleep 是 Thread 的。

18. synchronized vs ReentrantLock

维度
synchronized
ReentrantLock
锁获取
自动
手动 lock/unlock
可中断
不支持
lockInterruptibly()
超时
不支持
tryLock(timeout)
公平锁
非公平
可配公平/非公平
条件变量
一个
多个 Condition
结论:简单场景用 synchronized(JVM 优化得好,性能不差),需要高级特性用 ReentrantLock。

19. 死锁四个必要条件 + 排查

四个条件:互斥、持有并等待、不可剥夺、循环等待。破坏任意一个即可避免死锁。
排查:jstack  能看到线程的锁等待关系,直接标出 "Found one Java-level deadlock"。预防:固定加锁顺序、tryLock 加超时、避免嵌套锁。

五、Spring 源码:IOC/AOP 内核

20. IOC 和 AOP 到底是什么?

IOC(控制反转):把对象的创建和管理交给容器,配合 DI(依赖注入),解耦。核心容器 BeanFactory(懒加载)和 ApplicationContext(启动即加载,功能更全)
AOP(面向切面):把横切逻辑(日志、事务、权限)从业务里抽出来,通过动态代理织入。通知类型:@Before / @After / @Around / @AfterReturning / @AfterThrowing

21. Bean 生命周期(面试高概率)

实例化 → 属性注入(DI)→ 各种 Aware 接口(BeanNameAware、ApplicationContextAware)→ BeanPostProcessor#postProcessBeforeInitialization→ @PostConstruct/InitializingBean#afterPropertiesSet→ BeanPostProcessor#postProcessAfterInitialization(这里生成代理对象)→ 使用 → @PreDestroy/DisposableBean#destroy

22. Bean 作用域

singleton(默认,单例)、prototype(每次新建)、request、session、application(Web 环境)。
坑:prototype Bean 里注入 singleton Bean 没问题,但 singleton 里注入 prototype 时要小心,prototype 不会真的每次新建,需要 @Lookup 或 ObjectFactory 解决。

23. @Autowired vs @Resource

@Autowired
:Spring 提供,默认**按类型(byType)**注入,可配合 @Qualifier 按名称
@Resource
:JSR-250 标准,默认按名称(byName),找不到再按类型

24. Spring MVC 执行流程

DispatcherServlet(前端控制器)→ HandlerMapping(找处理器)→ HandlerAdapter(适配调用)→ Controller 返回 ModelAndView → ViewResolver(解析视图)→ 渲染响应。
拦截器(HandlerInterceptor)在框架层,过滤器(Filter)在 Servlet 容器层——拦截器能拿到 handler 和 ModelAndView,过滤器更底层。

六、MySQL 进阶:索引与事务的底层

25. 为什么索引用 B+ 树而不是 B 树或红黑树?

B+ 树非叶子节点只存索引不存数据,一个节点能存更多索引,树更矮,IO 次数更少
叶子节点用双向链表串起来,天然支持范围查询和排序
红黑树是二叉树,数据量大时树太高,IO 次数爆炸

26. 聚簇索引 vs 非聚簇索引

聚簇索引:叶子节点存整行数据,InnoDB 的主键索引就是聚簇索引,一张表只能有一个
非聚簇索引(二级索引):叶子节点存主键值,查非索引字段要回表
追问"为什么主键建议自增"?——自增主键插入有序,避免页分裂和随机 IO;随机主键会导致大量页分裂、碎片。

27. 事务隔离级别 + MVCC

四个级别:读未提交 → 读已提交 → 可重复读(InnoDB 默认)→ 串行化。
MVCC(多版本并发控制):通过 undo log 版本链 + ReadView 实现。可重复读和读已提交的区别就在于 ReadView 生成的时机不同——可重复读是事务开始时生成一次,读已提交是每次查询都重新生成。

28. redo log / undo log / binlog 三兄弟

redo log:物理日志,记录"做了什么修改",保证持久性,崩溃恢复用(WAL 先写日志再写盘)
undo log:逻辑日志,记录"修改前的数据",保证原子性(回滚)+ MVCC
binlog:逻辑日志,Server 层,用于主从复制数据恢复(redolog 是 InnoDB 层的)
两阶段提交:先写 redo log(prepare)→ 写 binlog → redo log 提交,保证两者一致。

29. InnoDB 的行锁、间隙锁、临键锁

行锁:锁住具体记录
间隙锁(Gap Lock):锁住一个区间,防止幻读
临键锁(Next-Key Lock):行锁 + 间隙锁,InnoDB 在可重复读下默认用的就是它,既锁记录又锁区间
这就是为什么 InnoDB 在可重复读下也能"解决"幻读——靠临键锁。

七、Redis 进阶:数据结构与高可用

30. 五大基本数据结构及场景

结构
底层
典型场景
String
SDS 动态字符串
缓存、计数器、分布式锁
Hash
哈希表/压缩列表
存对象、购物车
List
双向链表/压缩列表
消息队列、最新列表
Set
哈希表/整数集合
去重、共同好友
ZSet
跳表 + 哈希表
排行榜、延迟队列

31. RDB vs AOF 持久化

RDB:定时快照,文件小、恢复快,但两次快照之间可能丢数据
AOF:追加写日志,数据更安全(可配 everysec),但文件大、恢复慢
Redis 4.0 后支持混合持久化:RDB 做全量 + AOF 做增量,取两者之长

32. 内存淘汰策略

8 种,重点记:allkeys-lru(对全部 key 用 LRU)、volatile-lru(只对设了过期时间的 key)、allkeys-lfu、volatile-lfu、noeviction(默认,满了直接报错)。

33. 分布式锁怎么做?Redisson 看门狗是什么?

SET key value NX PX 30000 加锁 + Lua 脚本保证"判断+删除"原子性。
看门狗机制:Redisson 默认 30 秒锁,业务没执行完会自动续期(后台定时任务把锁续到 30 秒),防止业务还没跑完锁就过期,导致并发问题。主从切换可能丢锁,可考虑 RedLock(有争议)。

34. 缓存一致性:先删缓存还是先更新数据库?

推荐Cache Aside 模式先更新数据库,再删除缓存(不是更新缓存)。
为什么?——先删缓存会放大"缓存击穿窗口",且删缓存 + 更新 DB 之间若并发读到旧数据,又会把旧数据写回缓存,导致长时间不一致。配合延迟双删(更新后隔几百毫秒再删一次)兜底。

八、分布式 + 设计模式:架构题加分项

35. CAP 理论

一致性(C)、可用性(A)、分区容错性(P)三者不可兼得。分布式系统必须选 P,然后在 CP 或 AP 之间取舍——Zookeeper 选 CP(强一致,牺牲可用),Eureka/Nacos(AP) 选 AP(保证可用,接受短暂不一致)。

36. 分布式事务方案

2PC:两阶段提交,强一致但性能差、有阻塞风险
TCC:Try / Confirm / Cancel,业务侵入强但灵活
Seata AT 模式:基于 undo log 的自动补偿,侵入小,主流
本地消息表 / 事务消息(RocketMQ):最终一致性,解耦,适合跨服务异步场景

37. 消息队列三大用途

削峰(高并发秒杀)、解耦(系统之间不直接调用)、异步(非核心逻辑异步处理)。追问"消息丢了怎么办":生产端 confirm、MQ 持久化、消费端手动 ack + 重试,三点串联成"不丢消息"链路。

38. 设计模式在 Spring 中的体现

单例模式:Bean 默认 singleton
工厂模式:BeanFactory、FactoryBean
代理模式:AOP 动态代理
模板方法:JdbcTemplate、RestTemplate
观察者模式:Spring 事件 ApplicationEvent
策略模式:@Resource 注入不同实现类

🎯 最后

上一篇是"八股 + AI 考点"的广度,这一篇是"源码 + 底层原理"的深度。两篇合起来,广度深度都覆盖了。
面试逻辑其实就一条:基础题答准,源码题答深,场景题答活,项目题答细
别光收藏,挑几个自己最心虚的考点,打开源码/文档真看一遍,比背一百遍都管用。
关注我,持续更新 Java 面试干货 🔥