Appearance
面试八股
高频 Java 面试题汇总,按专题组织,每个问题用 2-3 句话精准回答。
一、Java 基础
Q1: String 为什么是不可变的?如何实现不可变?
A: String 类使用 final 修饰,底层用 private final byte[] 存储字符(JDK 9+),不提供任何修改方法。不可变保证了线程安全、字符串常量池复用和哈希缓存(hash 字段缓存了 hashCode)。所有"修改"操作(如 substring、replace)都返回新 String 对象。
Q2: equals() 和 hashCode() 的关系?
A: 如果 a.equals(b) 为 true,则 a.hashCode() 必须等于 b.hashCode()。反之不一定成立(哈希冲突)。重写 equals 必须重写 hashCode,否则 HashMap/HashSet 等散列集合会出现逻辑错误。它们共同保证对象在集合中的正确行为。
Q3: == 和 equals() 的区别?
A: == 比较的是引用地址(基本类型比较值),equals() 比较的是对象内容(需重写,默认也是比较引用)。对于 String 等已重写 equals 的类,equals 比较内容;对于未重写的类,equals 等同于 ==。
Q4: ArrayList 和 LinkedList 的区别?
A: ArrayList 底层是动态数组,支持 O(1) 随机访问,尾部插入快,中间插入/删除需要移动元素 O(n)。LinkedList 底层是双向链表,不支持随机访问 O(n),头尾插入/删除快 O(1)。查询密集用 ArrayList,频繁增删用 LinkedList。实际开发中 ArrayList 使用频率远高于 LinkedList。
Q5: HashMap 的实现原理?
A: JDK 8 中 HashMap 采用数组 + 链表 + 红黑树。通过 (n-1) & hash 计算桶位置,哈希冲突时用链表存储(拉链法),当链表长度 >= 8 且数组长度 >= 64 时转为红黑树(O(logn) 查询)。默认负载因子 0.75,当元素数量超过容量 * 负载因子时扩容为 2 倍。
Q6: HashMap 和 Hashtable 的区别?
A: HashMap 非线程安全,允许 null 键和值,效率高。Hashtable 线程安全(方法用 synchronized 修饰),不允许 null 键和值,已过时。需要线程安全的 Map 推荐使用 ConcurrentHashMap,分段锁机制比 Hashtable 全局锁更高效。
Q7: String、StringBuilder、StringBuffer 的区别?
A: String 不可变,每次拼接产生新对象。StringBuffer 可变且线程安全(方法 synchronized),适用于多线程环境字符串拼接。StringBuilder 可变但非线程安全,性能最高,单线程环境首选。循环中拼接字符串一定要用 StringBuilder/Buffer。
Q8: 接口和抽象类的区别?
A: 接口定义行为契约(能做什么),JDK 8+ 可以有 default 方法,支持多实现。抽象类定义共性模板(是什么),可以有构造方法、成员变量和具体方法,单继承。接口强调"like-a"关系,抽象类强调"is-a"关系。实际开发中优先使用接口。
Q9: 深拷贝和浅拷贝的区别?
A: 浅拷贝只复制对象本身,引用类型字段指向同一对象,修改会影响原对象(Object.clone() 默认行为)。深拷贝会递归复制所有引用类型字段,产生完全独立的对象。深拷贝可通过序列化/反序列化、手动递归复制或使用第三方库(如 Apache Commons Lang)实现。
Q10: 泛型中 ?、? extends T、? super T 的区别?
A: ? 是无界通配符,表示任意类型。? extends T 是上界通配符(生产者),只能读取不能写入(PECS 原则:Producer Extends)。? super T 是下界通配符(消费者),可以写入 T 或其子类,读取只能得到 Object。
二、异常
Q1: 受检异常和非受检异常的区别?
A: 受检异常(Checked Exception)继承自 Exception(非 RuntimeException),编译时必须 try-catch 或 throws 声明,如 IOException。非受检异常(Unchecked Exception)继承自 RuntimeException,编译时不强制处理,如 NullPointerException。受检异常体现"发生概率高且可恢复"的场景。
Q2: finally 块一定会执行吗?
A: 不一定。以下情况 finally 不执行:调用 System.exit() 终止 JVM,守护线程中所有非守护线程结束,线程被中断/杀死,CPU 断电等极端情况。正常情况下(return、break、异常抛出)finally 都会执行,且 return 语句在 finally 之前计算返回值,但 finally 的 return 会覆盖 try 中的返回值。
Q3: try-with-resources 的原理?
A: try-with-resources 是 Java 7 的语法糖,要求资源实现 AutoCloseable 接口。编译后会在 finally 块中自动调用 close() 方法。如果有多个资源,按声明顺序逆序关闭。close() 抛出的异常会被抑制(suppressed),可通过 Throwable.getSuppressed() 获取。
三、并发编程
Q1: synchronized 和 Lock 的区别?
A: synchronized 是 JVM 层面的关键字,自动获取和释放锁,不可中断,不支持公平锁,不支持条件唤醒。Lock 是 JDK 层面的接口,需要手动 lock()/unlock()(通常用 try-finally),支持尝试获取锁、可中断、公平锁、多条件变量。JDK 6 后 synchronized 经过了偏向锁、轻量级锁等优化,性能与 Lock 差距不大。
Q2: volatile 的作用?
A: volatile 保证可见性(一个线程修改后其他线程立即可见)和禁止指令重排序(内存屏障)。但不保证原子性,所以 i++ 这种复合操作仍然不是线程安全的。适用场景:状态标志位(如 running)、双重检查锁定中的单例、独立观察变量。
Q3: ThreadLocal 的原理和内存泄漏问题?
A: ThreadLocal 在每个线程的 ThreadLocalMap 中存储副本,key 是 ThreadLocal 的弱引用,value 是强引用。若不手动 remove(),即使 ThreadLocal 被 GC 回收,value 仍被 Thread 引用,在线程池场景下会导致内存泄漏。最佳实践:使用后必须调用 remove()。
Q4: 线程池的核心参数有哪些?
A: 七个参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程空闲存活时间)、unit(时间单位)、workQueue(阻塞队列)、threadFactory(线程工厂)、handler(拒绝策略)。执行流程:核心线程 -> 队列 -> 最大线程 -> 拒绝策略。
Q5: 线程池的拒绝策略有哪些?
A: JDK 内置四种:AbortPolicy(默认,抛异常 RejectedExecutionException)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃最早的任务)。实际开发中常用 CallerRunsPolicy(降级方案)或自定义拒绝策略。
Q6: ConcurrentHashMap 为什么线程安全?
A: JDK 7 使用分段锁(Segment),默认 16 个段,每个段独立加锁,最大并发度 16。JDK 8 使用 CAS + synchronized,锁粒度更细到桶级别,put 时 CAS 插入,冲突时 synchronized 锁住桶头节点,扩容时多线程协同迁移,并发性能大幅提升。
Q7: 线程的生命周期?
A: NEW(新建) -> RUNNABLE(就绪/运行) -> BLOCKED(等待锁) -> WAITING(无限等待,wait/join/park) -> TIMED_WAITING(超时等待,sleep/wait(timeout)) -> TERMINATED(终止)。RUNNABLE 是 JVM 层面的统一状态,包含了操作系统层面的就绪和运行两种状态。
四、JVM
Q1: JVM 内存结构是怎样的?
A: 分为线程私有和线程共享两部分。线程私有:程序计数器(当前执行字节码行号)、虚拟机栈(方法调用栈帧,存储局部变量、操作数栈等)、本地方法栈(Native 方法)。线程共享:堆(对象实例,GC 主要区域)、元空间(类信息、常量池、方法元数据,使用本地内存)。JDK 8 用元空间替代了永久代。
Q2: 堆内存的分代结构?
A: 堆分为新生代(Young Gen)和老年代(Old Gen)。新生代 = Eden(80%)+ Survivor0(10%)+ Survivor1(10%)。对象先在 Eden 分配,经过一次 Minor GC 后存活对象进入 Survivor,每熬过一次 GC 年龄 +1,默认 15 岁后进入老年代。大对象直接进入老年代(-XX:PretenureSizeThreshold)。
Q3: 常见的 GC 算法有哪些?
A: 标记-清除(碎片化,简单)、标记-复制(新生代,无碎片但浪费空间)、标记-整理(老年代,无碎片但耗时)。现代 GC 是分代收集——新生代用复制算法(少量存活对象),老年代用标记-清除或标记-整理算法。
Q4: 常见的垃圾回收器?
A: Serial(单线程,Client 模式)、Parallel(多线程,吞吐量优先,JDK 8 默认)、CMS(并发标记清除,低延迟,JDK 9+ 废弃)、G1(区域化,平衡吞吐量和延迟,JDK 9+ 默认)、ZGC(超低延迟,TB 级堆,JDK 11 实验,JDK 15 正式)、Shenandoah(低延迟,Red Hat 主导)。选择标准:桌面应用用 Serial,批处理用 Parallel,Web 服务用 G1/ZGC。
Q5: 类加载过程?
A: 三步:加载(读取字节码生成 Class 对象)-> 链接(验证 -> 准备 -> 解析)-> 初始化(执行 <clinit> 方法)。加载通过类加载器完成,双亲委派模型确保核心类不被篡改。打破双亲委派方式:重写 findClass()(推荐)或重写 loadClass()(如 Tomcat WebAppClassLoader)。
Q6: 双亲委派模型?
A: 类加载器层级:Bootstrap(加载核心类 rt.jar)-> Extension(JDK 8,加载 ext 目录)-> Application(加载 classpath)-> 自定义。加载时先向上委派,父加载器找不到才自己加载。目的:保证核心类库的类型安全,防止重复加载。
Q7: OOM 如何排查?
A: 首先加 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError 自动 dump 堆快照。使用 MAT/JProfiler 分析 dump 文件,定位占用内存最多的对象,分析 GC Root 引用链。常见原因:内存泄漏(集合持续增长、ThreadLocal 未清理)、大对象直接进入老年代、堆内存配置过小。同时检查 GC 日志判断是否是正常内存不足。
Q8: 强引用、软引用、弱引用、虚引用?
A: 强引用(Strong):Object obj = new Object(),宁可 OOM 也不回收。软引用(Soft):内存不足时回收,适合缓存。弱引用(Weak):GC 时立即回收,适合 WeakHashMap、ThreadLocal。虚引用(Phantom):最弱,无法通过它获取对象,用于跟踪对象回收状态(如 NIO 直接内存回收)。
五、Spring 框架
Q1: IoC 和 DI 的区别?
A: IoC(控制反转)是一种思想,将对象的创建和管理权从程序代码交给容器。DI(依赖注入)是 IoC 的实现方式,容器通过构造函数、setter 方法或字段注入将依赖对象注入到目标对象中。Spring 通过 ApplicationContext 管理 Bean 的生命周期和依赖关系。
Q2: Spring AOP 的原理?
A: AOP(面向切面编程)通过动态代理实现。如果目标对象实现了接口,使用 JDK 动态代理(Proxy.newProxyInstance);如果没有实现接口,使用 CGLIB 代理(字节码生成子类)。Spring Boot 2.x 默认使用 CGLIB。AOP 实现的核心概念:JoinPoint(连接点)、Pointcut(切入点)、Advice(通知)、Aspect(切面)。
Q3: Spring 事务的传播行为?
A: 七种传播行为:REQUIRED(默认,有则加入无则创建)、REQUIRES_NEW(总是新建事务,挂起当前)、SUPPORTS(有则加入无则非事务执行)、NOT_SUPPORTED(非事务执行,挂起当前)、MANDATORY(必须有事务,否则抛异常)、NEVER(必须非事务,否则抛异常)、NESTED(嵌套事务,支持回滚点)。最常用的是 REQUIRED 和 REQUIRES_NEW。
Q4: Spring Bean 的生命周期?
A: 实例化 -> 属性赋值(依赖注入)-> 各种 Aware 回调(BeanNameAware, BeanFactoryAware, ApplicationContextAware)-> BeanPostProcessor 前置处理 -> @PostConstruct 初始化方法 -> InitializingBean.afterPropertiesSet() -> BeanPostProcessor 后置处理(AOP 代理生成在此)-> Bean 就绪 -> @PreDestroy 销毁方法 -> DisposableBean.destroy()。
Q5: Spring Boot 自动配置原理?
A: 核心是 @SpringBootApplication 组合注解,包含 @EnableAutoConfiguration。通过 @Import(AutoConfigurationImportSelector.class) 加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中的配置类。配合 @Conditional 系列注解(如 @ConditionalOnClass、@ConditionalOnMissingBean),按条件创建 Bean。使用时引入 Starter 依赖即可自动配置。
Q6: Spring 中 Bean 的作用域?
A: singleton(默认,单例,整个容器共享一个实例)、prototype(原型,每次获取创建新实例)、request(每个 HTTP 请求一个实例)、session(每个 HTTP Session 一个实例)、application(ServletContext 级别单例)、websocket(WebSocket 级别)。最常用的是 singleton 和 prototype。
Q7: @Autowired 和 @Resource 的区别?
A: @Autowired 是 Spring 注解,默认按类型注入,配合 @Qualifier 按名称注入,required 属性可设为 false。@Resource 是 JDK 注解(javax.annotation),默认按名称注入,找不到再按类型。@Autowired 功能更强大,@Resource 更简洁,实际开发中 @Autowired 使用更广泛。
六、数据库
Q1: MySQL 索引底层数据结构是什么?
A: 默认使用 B+ 树。B+ 树特点:所有数据存储在叶子节点,非叶子节点只存索引值;叶子节点通过双向链表连接,支持范围查询;树的高度很低(3-4 层可存储千万级数据),磁盘 I/O 次数少。InnoDB 主键索引是聚簇索引(叶子节点存完整行数据),二级索引叶子节点存主键值(回表查询)。
Q2: 事务的四大特性(ACID)?
A: 原子性(Atomicity):事务是不可分割的最小单元,要么全成功要么全失败(通过 undo log 回滚)。一致性(Consistency):事务前后数据满足完整性约束。隔离性(Isolation):并发事务互不干扰(通过 MVCC + 锁实现)。持久性(Durability):事务提交后数据永久保存(通过 redo log 保证 crash-safe)。
Q3: 事务隔离级别和对应的并发问题?
A: 读未提交(Read Uncommitted):脏读、不可重复读、幻读。读已提交(Read Committed,Oracle 默认):不可重复读、幻读。可重复读(Repeatable Read,MySQL InnoDB 默认):幻读(InnoDB 通过间隙锁部分解决)。串行化(Serializable):无并发问题,性能最差。各级别用锁机制实现隔离。
Q4: MVCC 的原理?
A: MVCC(多版本并发控制)通过隐藏字段(DB_TRX_ID 事务 ID、DB_ROLL_PTR 回滚指针、DB_ROW_ID 行 ID)和 undo log 实现。每个事务读取时生成一个 ReadView,包含活跃事务列表。可见性判断:数据版本的事务 ID 小于 ReadView 的最小事务 ID 则可见,大于最大事务 ID 则不可见,在中间则判断是否在活跃列表中。MVCC 实现了读已提交和可重复读隔离级别下的一致读。
Q5: SQL 优化思路?
A: (1) 用 EXPLAIN 分析执行计划,关注 type(至少达到 range)、key(是否用索引)、rows(扫描行数)、Extra(filesort/temporary 需优化)。(2) 建立合适的索引,遵循最左前缀原则。(3) 避免 SELECT *,只取需要的列。(4) 避免在 WHERE 子句中对字段进行函数操作或计算。(5) 大表用分页时用延迟关联或基于主键的分页。(6) 合理使用连接查询,小表驱动大表。(7) 慢查询取日志分析。
七、分布式系统
Q1: CAP 理论是什么?
A: C(一致性,所有节点同一时刻数据一致)、A(可用性,请求总能得到响应)、P(分区容错性,网络分区时系统仍能工作)。分布式系统中 P 必须选择,因此实际是 CP 或 AP 的权衡。Nacos 支持 AP 和 CP 切换,Eureka 是 AP,ZooKeeper 是 CP,Redis Cluster 是 AP(最终一致性)。
Q2: 分布式锁如何实现?
A: 三种主流方案:(1) Redis:SET NX EX 原子命令,加锁设置过期时间,解锁用 Lua 脚本保证原子性(判断 + 删除),Redisson 提供了看门狗自动续期。(2) ZooKeeper:创建临时顺序节点,最小节点获得锁,通过 Watch 机制等待通知,Curator 提供了封装。(3) 数据库:基于唯一索引或乐观锁,性能差,不推荐。
Q3: 分布式事务解决方案?
A: 两阶段提交(2PC):协调者 -> 参与者 prepare -> 提交/回滚,强一致性但性能差,Seata AT 模式。TCC(Try-Confirm-Cancel):资源预留 -> 确认/取消,业务侵入性大。可靠消息最终一致性:本地事务 + 消息表,RocketMQ 事务消息。最大努力通知:定期重试,适用于对一致性要求不高的场景。Saga:长事务拆分为多个本地事务,每个事务有对应的补偿操作。
Q4: 什么是幂等性?如何保证?
A: 幂等性指同一操作执行一次和多次结果相同。(1) 唯一索引:数据库唯一约束,重复插入报错。(2) Token 机制:前端先获取 Token,提交时携带,服务端校验删除。(3) 状态机:支付状态 PREPAY -> PAYING -> PAID,已支付状态不再处理。(4) 乐观锁:版本号或时间戳,UPDATE ... SET version = version + 1 WHERE version = old_version。(5) 分布式锁:对业务唯一 ID 加锁。
Q5: 分布式 ID 生成方案?
A: (1) UUID:简单但无序,InnoDB 插入性能差。(2) 数据库自增:简单但存在单点瓶颈。(3) 号段模式:批量从数据库获取 ID 范围,用完再取,美团 Leaf。(4) 雪花算法(Snowflake):64 位,时间戳 + 机器 ID + 序列号,有序递增,依赖时钟同步(时钟回拨是痛点)。(5) Redis 自增:INCR 命令,性能高但需持久化。
八、中间件
Q1: Redis 为什么这么快?
A: (1) 纯内存操作,数据存储在内存中。(2) 单线程模型(命令处理单线程),避免上下文切换和锁竞争(Redis 6.0 引入多线程 I/O 处理)。(3) IO 多路复用,epoll 模型处理大量并发连接。(4) 高效的数据结构,如 SDS 简单动态字符串、ziplist 压缩列表、跳表等。(5) RESP 协议简单高效。
Q2: Redis 缓存穿透、击穿、雪崩?
A: 缓存穿透:查询不存在的数据,穿透到 DB。方案:布隆过滤器、缓存空值(短过期时间)。缓存击穿:热点 key 过期瞬间大量请求打 DB。方案:互斥锁更新、逻辑过期(不过期,后台异步更新)。缓存雪崩:大量 key 同时过期或 Redis 宕机。方案:过期时间加随机值、Redis 集群高可用、限流降级、多级缓存。
Q3: 消息队列的作用?如何保证消息不丢失?
A: 作用:解耦(系统间松耦合)、异步(非核心流程异步处理)、削峰(平滑流量峰值)。消息不丢失:(1) 生产端:发送确认机制(同步/异步回调)。(2) Broker 端:持久化(RocketMQ 同步刷盘,Kafka 多副本 ISR 确认)。(3) 消费端:手动提交 offset,业务处理完成后再确认。
附录:面试回答框架
| 层面 | 回答思路 |
|---|---|
| 是什么 | 一句话定义核心概念 |
| 为什么 | 解决了什么问题,为什么要用它 |
| 怎么做 | 底层原理或实现方式(1-2 个关键点) |
| 对比 | 与同类技术的区别(如 HashMap vs ConcurrentHashMap) |
| 坑 | 常见问题和注意事项 |
