Java 后端面试通常不会只考察 API 是否熟悉,还会继续追问底层原理、并发安全、线上问题排查和系统设计。本文按照“基础知识 → 并发编程 → JVM → Spring → Web 与场景题”的顺序整理,适合作为面试前的复习清单。
一、面试答题思路
遇到原理题时,可以按照下面的顺序组织答案:
- 先给出结论,避免一开始就陷入细节。
- 再解释实现原理,说明关键数据结构或执行流程。
- 最后补充适用场景、限制条件和实际排查方法。
例如回答“为什么使用线程池”时,不能只说“提高性能”,还应该说明线程复用、统一管理、任务排队和过载保护分别解决了什么问题。
二、Java 基础
1. == 和 equals() 有什么区别?
== 用于比较基本类型的值;如果比较的是对象,则比较两个引用是否指向同一个对象。equals() 默认实现与 == 相同,但很多类会重写它,用于比较对象的逻辑内容,例如 String。
如果重写了 equals(),通常也必须重写 hashCode(),并保证两个相等对象的哈希值相同,否则放入 HashMap 或 HashSet 后可能出现查找失败或重复元素问题。
2. String 为什么设计成不可变类?
String 的底层内容创建后不能被修改。这样设计主要有几个原因:
- 字符串常量池可以安全地复用对象。
- 字符串可以缓存哈希值,适合作为
HashMap的键。 - 在类加载、文件路径和网络连接等场景中,不可变对象更安全。
- 多线程共享字符串时不需要额外加锁。
如果需要频繁拼接字符串,应优先使用 StringBuilder;多线程共享并修改同一个缓冲区时,可以考虑 StringBuffer,但它的同步开销更高。
3. Java 是值传递还是引用传递?
Java 只有值传递。传递基本类型时,复制的是具体的值;传递对象时,复制的是对象引用的值,因此方法内部可以通过这个引用修改对象的成员,但不能让调用方的引用指向另一个对象。
1 | void change(User user) { |
4. final、finally、finalize 有什么区别?
final可以修饰变量、方法和类,分别表示不可重新赋值、不可重写和不可继承。finally是异常处理语句的一部分,通常用于释放资源。使用try-with-resources管理实现了AutoCloseable的资源更可靠。finalize()曾被用于对象回收前的清理,但执行时机不可控,已不适合作为资源释放方案。
5. 接口和抽象类如何选择?
抽象类适合表达具有共同状态或基础实现的一组对象;接口适合表达某种能力或约定。一个类只能继承一个父类,但可以实现多个接口,因此接口更适合组合不同能力。
实际项目中可以使用接口定义业务边界,用抽象类复用模板流程,用组合代替过深的继承层次。
三、集合框架
1. ArrayList 和 LinkedList 的区别
ArrayList 基于动态数组,随机访问快,尾部追加通常效率较高;在中间插入或删除时,需要移动后续元素。LinkedList 基于双向链表,已定位到节点后插入和删除较快,但随机访问需要从链表中遍历,且节点对象会带来额外内存开销。
大多数业务场景优先使用 ArrayList。只有在明确存在大量链表式插入删除,并且能够高效定位节点时,才考虑 LinkedList。
2. HashMap 的底层结构是什么?
HashMap 使用数组作为桶,数组中的元素通常是链表节点;当同一个桶中的节点数量达到条件后,链表可能转为红黑树,以降低极端哈希冲突下的查询复杂度。
一次 put 的大致过程如下:
- 根据 key 的哈希值计算桶下标。
- 桶为空时直接插入。
- 桶不为空时,比较哈希值和 key 是否相等。
- 如果发生冲突,则在链表或红黑树中查找或插入。
- 元素数量达到扩容阈值时,重新分配桶并迁移元素。
3. HashMap 为什么需要扩容?
元素过多会导致桶中的冲突增多,查询效率下降。HashMap 使用负载因子控制容量与冲突之间的平衡:容量达到“容量 × 负载因子”时扩容。扩容通常会创建更大的数组,并重新计算元素所在的桶。
因此,如果能够预估数据量,可以在创建集合时设置合适的初始容量,减少扩容和迁移带来的开销。
4. HashMap 和 ConcurrentHashMap 有什么区别?
HashMap 不是线程安全的,在并发写入时可能出现数据覆盖、结构异常等问题。ConcurrentHashMap 针对并发读写进行了设计,读操作通常不需要锁,写操作只锁定必要的局部范围,并结合 CAS 等机制降低竞争。
不要简单地把共享的 HashMap 替换成 ConcurrentHashMap 就认为业务安全了。如果“查询后再写入”是一个整体业务动作,仍然需要保证整个操作的原子性。
四、并发编程
1. synchronized 的作用是什么?
synchronized 可以保证同一时刻只有一个线程进入临界区,同时保证锁释放前的修改对后续获得同一把锁的线程可见。它既提供互斥,也提供内存可见性和一定程度的有序性保证。
锁的对象必须稳定且可被共享。不要使用字符串常量、可变对象或对外暴露的对象作为锁,否则容易造成锁竞争或被其他代码意外持有。
2. volatile 能保证什么?
volatile 可以保证变量的可见性,并限制特定情况下的指令重排序,但不能保证复合操作的原子性。
1 | volatile boolean stopped; |
多个线程读取 stopped 时能够及时看到最新值,但 count++ 仍然包含读取、加一、写回三个步骤,不能仅靠 volatile 保证线程安全。
3. synchronized 和 ReentrantLock 如何选择?
synchronized 语法简单,异常退出时会自动释放锁,适合大多数互斥场景。ReentrantLock 提供了更丰富的能力,例如可中断获取锁、尝试获取锁、公平锁和多个条件队列,但需要手动在 finally 中释放。
1 | lock.lock(); |
选择时应优先考虑代码的清晰度和业务需求,而不是单纯比较两者的理论性能。
4. ThreadLocal 有什么作用?
ThreadLocal 为每个线程提供独立的变量副本,常用于保存请求上下文、用户信息或数据库连接相关的线程上下文。
在线程池中使用时要特别注意清理:线程会被复用,如果不调用 remove(),上一个请求的数据可能泄漏给下一个请求,也可能因为线程长期存活而造成内存占用。
5. 线程池的核心参数有哪些?
ThreadPoolExecutor 的重要参数包括:
corePoolSize:核心线程数。maximumPoolSize:最大线程数。keepAliveTime:非核心线程的空闲存活时间。workQueue:等待执行的任务队列。threadFactory:线程创建工厂。handler:任务无法接收时的拒绝策略。
任务提交后的典型流程是:核心线程未满时创建核心线程;核心线程已满时进入队列;队列已满且线程数未达到最大值时创建非核心线程;线程和队列都无法接收时执行拒绝策略。
6. 线程池的拒绝策略有哪些?
AbortPolicy:抛出异常,适合不允许静默丢任务的场景。CallerRunsPolicy:由提交任务的线程执行任务,可以形成一定的反向压力。DiscardPolicy:直接丢弃任务,不抛出异常。DiscardOldestPolicy:丢弃队列中等待时间最长的任务,再尝试提交新任务。
拒绝策略不能脱离业务选择。订单、支付等关键任务通常不能静默丢弃;日志、监控等非关键任务可以考虑降级、丢弃或转存。
7. 如何设置线程池参数?
CPU 密集型任务更关注 CPU 利用率,线程数通常接近 CPU 核数;IO 密集型任务会频繁等待网络或磁盘,可以设置更多线程,但必须结合外部服务承载能力、队列长度和超时时间进行压测。
不要直接使用无界队列或 Executors 创建不可控的线程池。生产环境建议显式创建线程池,设置有界队列、线程命名、拒绝策略和监控指标。
五、JVM
1. JVM 运行时内存区域有哪些?
- 程序计数器:记录当前线程下一条要执行的字节码位置。
- 虚拟机栈:保存方法调用、局部变量和操作数栈等信息。
- 本地方法栈:服务于本地方法调用。
- 堆:存放对象实例,是垃圾回收的主要区域。
- 方法区:存放类元数据、运行时常量池等信息,具体实现与 JVM 版本有关。
程序计数器和虚拟机栈通常是线程私有的,堆和方法区通常由多个线程共享。
2. 对象创建的大致过程
执行 new 时,JVM 通常会先检查类是否已经加载;随后为对象分配内存、初始化对象头、执行零值初始化,并调用构造方法完成显式初始化。分配内存时可能涉及指针碰撞或空闲列表,具体方式与垃圾回收器和堆布局有关。
3. Minor GC、Major GC 和 Full GC 有什么区别?
Minor GC 通常指新生代回收,频率较高、停顿时间相对较短。Major GC 通常与老年代回收相关,不同收集器对术语的定义并不完全一致。Full GC 往往会回收整个堆,甚至涉及方法区,通常停顿时间更长。
排查 GC 问题时不能只看次数,还应结合停顿时间、回收前后内存、对象分配速率、晋升速度和业务流量一起判断。
4. 双亲委派模型解决什么问题?
类加载器收到加载请求时,通常先让父加载器尝试加载,父加载器无法完成时子加载器才尝试。这样可以避免核心类被业务类覆盖,也可以减少同一个类被重复加载。
在需要隔离不同版本依赖或实现插件化时,可能会使用自定义类加载器,但必须处理好类的可见性和资源释放问题。
5. 线上频繁 Full GC 如何排查?
可以按照以下顺序排查:
- 查看 GC 日志,确认停顿时间和回收前后的内存变化。
- 检查堆大小、新生代大小和对象晋升速度。
- 使用堆转储或对象统计工具定位大对象和异常增长的集合。
- 检查是否存在缓存无限增长、ThreadLocal 未清理或监听器未注销。
- 结合接口流量和发布记录,确认是否是业务流量或代码变更导致。
调大堆内存只能延缓问题,不能替代对象生命周期和数据结构的治理。
六、Spring 与 Spring Boot
1. Spring IOC 和 AOP 是什么?
IOC 是控制反转:对象的创建、依赖组装和生命周期管理交给 Spring 容器。AOP 是面向切面编程:把日志、事务、权限等横切逻辑从业务代码中抽离,通过代理在指定连接点执行。
2. Bean 的生命周期是什么?
简化流程可以概括为:实例化 → 属性注入 → 执行各种 Bean 后置处理器 → 初始化方法 → Bean 可以使用 → 容器销毁时执行销毁方法。
实际项目中,@PostConstruct、InitializingBean 和自定义初始化方法都可能参与初始化阶段。理解生命周期有助于排查依赖未注入、代理未生效和初始化顺序问题。
3. Spring 如何解决单例 Bean 的循环依赖?
对于部分单例 Bean 的属性注入循环依赖,Spring 可以通过提前暴露对象引用,并借助三级缓存完成依赖注入。构造器循环依赖通常无法通过这种方式解决,因为对象尚未完成实例化。
循环依赖往往说明职责划分不清。即使框架能够处理,也应优先通过拆分职责、引入中间接口或调整依赖方向来消除它。
4. Spring 事务为什么会失效?
常见原因包括:
- 方法不是由 Spring 管理的 Bean 调用。
- 事务方法不是
public,或代理无法拦截。 - 同一个类内部直接调用事务方法,没有经过代理对象。
- 异常被捕获后没有重新抛出。
- 抛出的异常类型不符合默认回滚规则。
- 数据库引擎或连接配置不支持事务。
事务边界应该围绕完整的业务操作设计,避免把远程调用、耗时任务和不必要的查询长时间放在同一个事务中。
5. Spring Boot 自动配置是如何工作的?
Spring Boot 根据类路径中的依赖、配置文件和条件注解决定是否创建配置类和 Bean。常见条件包括某个类是否存在、某个 Bean 是否已经存在、某个配置属性是否开启等。
遇到自动配置不符合预期时,可以查看启动日志中的条件评估报告,确认哪些条件匹配、哪些配置被覆盖。
七、Web 与微服务
1. Cookie、Session、Token 和 JWT 有什么区别?
Cookie 是浏览器保存和发送的小块数据;Session 通常把会话状态保存在服务端,再通过 Cookie 保存会话标识;Token 把认证信息交给客户端携带,服务端可以采用无状态校验;JWT 是一种包含声明并经过签名的 Token 格式。
JWT 并不等于加密,也不能天然解决注销和主动失效问题。对安全性要求较高的场景,需要结合短过期时间、刷新机制、撤销列表和安全传输策略。
2. 如何保证接口幂等?
常用方法包括:
- 使用业务唯一号,并在数据库建立唯一约束。
- 使用幂等 Token,服务端只允许成功消费一次。
- 通过状态机限制状态的合法流转。
- 对消息记录消费结果,重复消息直接返回历史结果。
- 使用分布式锁控制短时间内的重复请求。
幂等的关键不是让请求只执行一次,而是让同一个业务请求执行多次后结果仍然一致。
3. 服务雪崩、降级、限流和熔断分别是什么?
- 雪崩:大量请求因为某个依赖故障或资源耗尽而连锁失败。
- 降级:主动关闭非核心功能,优先保障核心链路。
- 限流:限制单位时间内的请求量,保护服务资源。
- 熔断:依赖错误率或超时达到阈值后,暂时停止调用,经过恢复窗口后再尝试。
这些机制通常需要配合超时、重试上限、隔离线程池和监控告警使用,单独重试可能进一步放大故障。
八、综合场景题
1. 接口响应慢如何排查?
先通过链路追踪或日志确认慢在网关、应用、数据库还是下游服务。然后分别检查线程池是否耗尽、锁竞争、慢 SQL、连接池等待、网络超时、序列化开销和 GC 停顿。排查时要区分平均耗时和 P99、P999 等尾部延迟。
2. 如何设计高并发接口?
可以从以下方面拆解:
- 使用缓存减少数据库读压力。
- 通过限流、排队和快速失败保护系统。
- 将耗时任务异步化,缩短同步请求链路。
- 使用连接池和线程池,但设置合理上限。
- 通过数据库索引、读写分离或分片提升存储能力。
- 对关键操作设计幂等、降级和补偿机制。
高并发设计不能只追求吞吐量,还要明确数据一致性、可用性、失败恢复和运维成本之间的取舍。
九、总结
Java 后端面试的重点不是背诵孤立的定义,而是能够把语言特性、并发模型、JVM 运行机制和业务场景联系起来。复习时可以优先掌握以下主线:
- Java 对象和集合是如何工作的。
- 并发代码如何保证安全、可见和可控。
- JVM 如何分配对象并回收内存。
- Spring 如何管理对象和事务。
- 线上问题如何通过指标、日志和链路逐层定位。
