开场说明
这是一场面向 1-3 年 Java 后端候选人的大厂一面模拟,时长约 30 分钟。面试官风格贴近真实大厂节奏:先问结论,再追源码;先看广度,再挖边界;最后落到线上场景与取舍。候选人需具备扎实的 Java 基础、对主流框架和中间件有实际使用经验,并能结合项目回答设计权衡。
本文将完整还原面试过程,包含 8 个主问题、4 条重点追问链,覆盖 JVM、Spring、MySQL、Redis、MQ 五大模块,强调"原理 → 边界 → 故障 → 取舍"的递进逻辑。
主问题部分
1. 你能说说 JVM 类加载机制吗?双亲委派模型是做什么的?
参考回答: 类加载分为加载、验证、准备、解析、初始化五个阶段。双亲委派模型是指类加载器在收到加载请求时,先委托父类加载器尝试加载,只有当父类无法完成时,才由自己加载。这样可以保证核心类(如 java.lang.Object)只被 Bootstrap ClassLoader 加载一次,避免用户自定义同名类覆盖 JDK 类。
追问点: 如果我要打破双亲委派,怎么做?Tomcat 是怎么做的?
2. Spring 中 Bean 的生命周期是怎样的?AOP 代理是在哪个阶段创建的?
参考回答: Bean 生命周期包括:实例化 → 属性赋值 → 初始化前(BeanPostProcessor)→ 初始化(InitializingBean / @PostConstruct)→ 初始化后(AOP 代理生成)→ 使用 → 销毁。AOP 代理通常在初始化后的 BeanPostProcessor 中通过 AbstractAutoProxyCreator 创建,基于 JDK 动态代理或 CGLIB。
追问点: 为什么默认情况下同类内部方法调用不走 AOP?如何解决?
3. MySQL 的 InnoDB 是如何实现事务隔离的?可重复读下怎么解决幻读?
参考回答: InnoDB 通过 MVCC(多版本并发控制)和锁机制实现隔离级别。在可重复读(RR)下,通过间隙锁(Gap Lock)防止其他事务插入新记录,从而避免幻读。读操作使用快照读(一致性非锁定读),写操作加行锁 + 间隙锁。
追问点: 间隙锁在什么情况下会升级为临键锁?会不会导致死锁?
4. Redis 做缓存时,如何避免缓存雪崩?你项目里是怎么做的?
参考回答: 缓存雪崩是大量 key 同时过期导致请求打到数据库。我们采用三种策略:
- 过期时间加随机值(如 300s ± 60s)
- 多级缓存(本地 Caffeine + Redis)
- 热点 key 永不过期,通过后台任务异步刷新
追问点: 如果热点 key 突然失效,大量请求并发重建缓存,怎么处理?
5. Kafka 如何保证消息不丢失?生产者、Broker、消费者各需要注意什么?
参考回答:
- 生产者:设置 acks=all,启用重试,配合 idempotence 防重复
- Broker:min.insync.replicas ≥ 2,避免单副本写入
- 消费者:手动提交 offset,处理完业务再提交,避免自动提交导致丢消息
追问点: 如果消费者处理消息时崩溃,offset 已提交但业务未落库,怎么办?
6. 你用过 ThreadLocal 吗?它有什么内存泄漏风险?怎么规避?
参考回答: ThreadLocal 通过 ThreadLocalMap 存储数据,key 是弱引用,value 是强引用。如果线程长期存活(如线程池),value 无法被 GC,导致内存泄漏。规避方式:使用后调用 remove(),或避免在线程池中滥用。
追问点: 为什么 key 设计成弱引用而不是强引用?
7. 项目中遇到过 OOM 吗?怎么排查的?
参考回答: 曾遇到一次 Young GC 频繁导致卡顿。通过 jstat 观察 GC 日志,发现 Eden 区增长过快;再用 jmap 生成堆转储,MAT 分析发现是某个缓存未设上限,不断累积大对象。最终加上 LRU 淘汰策略和大小限制解决。
追问点: 如果是 Metaspace OOM,可能是什么原因?怎么调优?
8. 设计一个高并发抢红包系统,你会怎么考虑缓存和一致性?
参考回答:
- 红包总额预加载到 Redis,原子扣减(DECRBY)
- 用户领取记录写入 MySQL,通过唯一索引防重
- 异步消息通知账务系统,保证最终一致
- 本地缓存用户当日领取次数,减少 DB 查询
追问点: 如果 Redis 扣减成功但 DB 写入失败,怎么回滚?
追问部分
追问链 1:类加载器与热部署
面试官:你说 Tomcat 打破了双亲委派,具体是怎么实现的?
候选人:Tomcat 为每个 WebApp 创建 WebAppClassLoader,它优先自己加载 WEB-INF/classes 和 lib 下的类,只有找不到时才委托父加载器。这样不同应用可以加载同名不同版本的 jar。
面试官:那如果两个应用依赖同一个库的不同版本,会不会冲突?
候选人:不会,因为每个 WebAppClassLoader 独立,类加载隔离。但共享库(如 JDBC 驱动)仍由 CommonClassLoader 加载,需谨慎管理。
面试官:线上曾出现类冲突导致 NoSuchMethodError,你怎么定位?
候选人:用 -verbose:class 看加载路径,或用 arthas 的 classloader 命令查看类来源,确认是否被错误版本覆盖。
追问链 2:AOP 自调用问题
面试官:你说同类内部调用不走 AOP,为什么?
候选人:因为 Spring AOP 基于代理,内部调用 this.method() 直接走目标对象,不经过代理。
面试官:那怎么解决?
候选人:三种方式:
- 通过 ApplicationContext 获取代理 Bean 再调用
- 使用 AspectJ 编译期织入(LTW)
- 重构代码,将需要切面的方法抽到新类
面试官:你们项目选了哪种?为什么?
候选人:选了第三种,因为 AspectJ 配置复杂,且重构后职责更清晰,符合 Clean Architecture。
追问链 3:Redis 热点 Key 重建竞争
面试官:你说热点 key 失效后并发重建有问题,具体怎么解决?
候选人:可以用分布式锁(Redisson)控制只有一个线程重建,其他线程等待或返回旧值。
面试官:但加锁本身有性能开销,有没有无锁方案?
候选人:可以用"缓存空对象"或"永不过期 + 异步刷新":key 不过期,后台定时任务提前刷新,前台读始终命中。
面试官:如果异步刷新失败呢?
候选人:那就降级为同步重建,但加本地锁(synchronized)限制单机并发,避免雪崩。
追问链 4:Kafka 消费幂等与对账
面试官:你说消费者要手动提交 offset,那如何保证业务幂等?
候选人:在业务表中加唯一键(如消息 ID + 业务类型),插入前判断是否已处理。
面试官:如果消息重复但业务逻辑复杂,唯一键不好设计呢?
候选人:可以用 Redis 记录已处理消息 ID,设置合理过期时间;或引入对账系统,定时扫描未确认消息重新处理。
面试官:对账系统怎么设计?
候选人:每天定时任务拉取 Kafka 消息轨迹和 DB 状态,对比差异,触发补偿。关键是要有消息唯一 ID 和全链路追踪。
面试点评
本场面试重点考察候选人对核心机制的理解深度和线上问题应对能力。
- 易卡点 1:双亲委派的打破场景不熟悉,多数候选人只知 Tomcat,不知 Jetty 或 OSGi 也有类似机制。
- 易卡点 2:AOP 自调用问题常出现在事务失效场景,候选人容易忽略代理本质。
- 易卡点 3:缓存重建策略停留在"加锁"层面,缺乏无锁或降级方案思考。
- 易卡点 4:消息幂等设计过于依赖数据库唯一键,未考虑对账补偿等最终一致手段。
整体来看,候选人需加强"边界条件"和"故障兜底"思维,避免只答理想路径。
技术补丁包
-
双亲委派模型 原理:类加载请求优先委托父加载器,防止核心类被替换。 设计动机:安全性与类唯一性保障。 边界条件:自定义类加载器可打破,但需谨慎处理类冲突。 落地建议:Tomcat 通过 WebAppClassLoader 实现应用隔离,共享库由 CommonClassLoader 加载。
-
Spring AOP 代理机制 原理:基于 JDK 动态代理或 CGLIB 生成代理对象,在方法前后插入切面逻辑。 设计动机:解耦横切关注点(如日志、事务)。 边界条件:同类内部调用不走代理,需通过外部调用或重构解决。 落地建议:复杂切面考虑使用 AspectJ,简单场景优先代码重构。
-
InnoDB 间隙锁 原理:在可重复读隔离级别下,对索引范围加锁,防止幻读。 设计动机:保证事务内多次读取结果一致。 边界条件:间隙锁可能引发死锁,需合理设计索引和事务粒度。 落地建议:避免长事务,尽量使用覆盖索引减少锁范围。
-
Redis 缓存雪崩防护 原理:通过随机过期、多级缓存、热点永不过期等手段分散失效风险。 设计动机:避免瞬时 DB 压力激增。 边界条件:热点 key 重建可能引发并发竞争。 落地建议:结合本地缓存 + 异步刷新 + 降级策略构建多层防护。
-
Kafka 消息可靠性保障 原理:生产者 ack=all、Broker 多副本、消费者手动提交 offset。 设计动机:端到端消息不丢失。 边界条件:消费者崩溃可能导致业务未处理但 offset 已提交。 落地建议:业务层实现幂等 + 对账补偿机制。
-
ThreadLocal 内存泄漏 原理:ThreadLocalMap 的 value 是强引用,线程池中长期存活导致无法 GC。 设计动机:线程内数据隔离。 边界条件:不调用 remove() 极易泄漏。 落地建议:始终在 finally 块中调用 remove(),避免在线程池中使用。
-
OOM 排查流程 原理:通过 GC 日志、堆转储、内存分析工具定位泄漏点。 设计动机:快速恢复服务并根因分析。 边界条件:Metaspace OOM 常因动态生成类过多(如 CGLIB、反射)。 落地建议:开启 -XX:+HeapDumpOnOutOfMemoryError,配合 MAT 分析支配树。
-
分布式锁选型 原理:基于 Redis(Redisson)或 ZooKeeper 实现互斥访问。 设计动机:控制共享资源并发访问。 边界条件:Redis 锁需处理过期时间、续约、脑裂问题。 落地建议:优先使用 Redisson 的看门狗机制自动续约,避免业务超时锁失效。
-
消息幂等设计 原理:通过唯一标识判断消息是否已处理。 设计动机:防止重复消费导致数据错误。 边界条件:唯一键难设计时需引入外部状态存储。 落地建议:结合 Redis 去重 + 数据库唯一约束 + 对账补偿三重保障。
-
缓存一致性策略 原理:Cache-Aside、Read-Through、Write-Through 等模式协调缓存与 DB。 设计动机:平衡性能与数据准确性。 边界条件:缓存更新失败可能导致脏读。 落地建议:写操作先更新 DB 再删缓存,配合延迟双删减少不一致窗口。
-
线程池拒绝策略 原理:当队列满且线程数达上限时,触发拒绝策略(Abort、CallerRuns 等)。 设计动机:防止系统过载。 边界条件:CallerRuns 可能拖慢主线程,需评估业务容忍度。 落地建议:关键业务使用有界队列 + CallerRuns,非关键业务用 Abort 并记录日志。
-
MySQL 死锁检测 原理:InnoDB 通过 wait-for graph 检测死锁,回滚代价最小的事务。 设计动机:自动解除循环等待。 边界条件:高并发下死锁频繁需优化 SQL 和事务顺序。 落地建议:统一访问顺序,减少事务粒度,开启 innodb_deadlock_detect。
-
G1 GC 调优 原理:分 region 回收,可预测停顿时间。 设计动机:替代 CMS,适合大堆低延迟场景。 边界条件:Mixed GC 阶段可能因 Humongous 对象失败。 落地建议:设置 -XX:MaxGCPauseMillis,监控 Evacuation Failure。
-
Spring 事务传播行为 原理:定义事务方法调用其他事务方法时的行为(REQUIRED、REQUIRES_NEW 等)。 设计动机:灵活控制事务边界。 边界条件:PROPAGATION_REQUIRES_NEW 会挂起当前事务,可能影响性能。 落地建议:避免嵌套事务过深,优先使用 REQUIRED。
-
布隆过滤器应用 原理:概率型数据结构,判断元素是否可能存在。 设计动机:高效过滤不存在 key,防止缓存穿透。 边界条件:存在误判率,不能删除元素。 落地建议:用于读多写少场景,配合 Redis 缓存使用。
-
本地缓存选型 原理:Caffeine 基于 W-TinyLFU 算法实现高性能缓存。 设计动机:减少远程调用,提升响应速度。 边界条件:集群环境下需解决缓存一致性问题。 落地建议:用于读多写少、容忍短暂不一致的数据(如配置、用户基础信息)。
-
分布式事务最终一致性 原理:通过消息队列 + 本地事务表 + 对账实现最终一致。 设计动机:避免强一致带来的性能损耗。 边界条件:对账延迟可能导致短暂不一致。 落地建议:关键业务(如支付)采用 TCC,一般业务用消息 + 对账。
-
慢 SQL 优化路径 原理:通过 explain 分析执行计划,优化索引和查询结构。 设计动机:降低数据库负载,提升响应速度。 边界条件:索引过多影响写入性能。 落地建议:定期 review 慢查询日志,避免 select *,使用覆盖索引。
-
Redisson 分布式锁实现 原理:基于 Lua 脚本保证原子性,看门狗线程自动续期。 设计动机:简化分布式锁使用复杂度。 边界条件:Redis 主从切换可能导致锁失效(RedLock 可缓解)。 落地建议:业务超时时间应大于锁租期,避免并发执行。
-
全链路追踪集成 原理:通过 traceId 串联服务调用链,定位性能瓶颈。 设计动机:提升可观测性,快速定位问题。 边界条件:采样率设置影响性能与数据完整性。 落地建议:集成 SkyWalking 或 Arthas,关键路径全采样。