# 01.面试八股
# Java基础
# 🟢 1. HashMap 的 put 流程(简述)
| 步骤 | 关键动作 | 核心逻辑/公式 |
|---|---|---|
| 1. 计算哈希 | 扰动函数 | hash = (h = key.hashCode()) ^ (h >>> 16) |
| 2. 定位索引 | 计算数组下标 | index = (n - 1) & hash (n为数组长度) |
| 3. 插入/冲突 | 判断桶是否为空 | 空则直接插入;非空则处理冲突(覆盖、链表、红黑树) |
| 4. 树化判断 | 链表转红黑树 | 链表长度 ≥ 8 且 数组长度 ≥ 64 |
| 5. 扩容检查 | 检查负载因子 | 元素个数 > 阈值 (容量 * 0.75) 则扩容 |
面试加分点
1.为什么用尾插法?
因为HashMap是线程不安全的,如果用头插法的话,两个线程同时去操作HashMap,可能会导致死循环,占用CPU,比如说,一个节点下有一个A->B的链表,线程1去操作,链表变成B->A,但是线程1还没执行完,线程2也去扩容,线程2还是显示A->B,这时候线程1执行完了,线程2开始扩容了,但是他不知道线程1已经改变了链表结构,导致死循环
2.为什么扩容是2的n次方?
1)进行与操作比取模的速度快,而2的n次方-1与哈希值进行与效率更高
2)减少哈希冲突,2^n-1,进行与操作保证了哈希值的每一位都参与运算,哈希分布更均匀
3) 扩容迁移的优化 (JDK 1.8)
在 JDK 1.8 中,由于容量是 2 倍扩容,元素在迁移时,其在新数组中的位置只有两种可能:
- 留在原索引位置
- 移动到
原索引 + 旧容量的新位置
3.时间复杂度是多少?
1)最好/平均情况:O(1)(直接定位到桶)。
2)最坏情况:O(log n)(发生冲突转为红黑树后)。JDK 1.7 中最坏是 O(n)遍历链表。
4.为什么负载因子是 0.75?
负载因子太小,扩容频繁浪费空间
太大的话,哈希冲突频繁,影响性能
# 🟡 2. HashMap 的扩容机制
问:HashMap 什么时候会扩容?扩容时元素如何重新分布?
触发条件:当元素数量>数组长度*负载因子(0.75)时
扩容过程:扩容为原来的2倍==>重新计算元素的哈希值==>分配到新数组
优化点:元素在新数组中的位置=原索引或原索引+旧容量
# 🟢 3. String 为什么设计成不可变的?
问:Java 中 String 为什么是 final 的?不可变性带来了哪些好处?
原因:因为char 数组是final 且private,没有提供修改方法
好处:
1)字符串常量池可共享节省内存,如果池中有这个字符串,直接返回避免重复创建,没有就创建一个新的,存到常量池
2)天生线程安全,不会因为多线程,而发生系统崩溃
3)字符串常量作为集合的键,其哈希值会被缓存,提升集合操作的效率
4)出于一些文件路径和URL的考虑,将字符串设置成final更合适一些
# 🟡 4. 异常体系与自定义异常
问:说说 Java 中 Error 和 Exception 的区别。什么时候需要自定义异常?
1)Error:JVM内部错误,如OOM内存溢出,StackOverFlow栈溢出,程序无法处理
OOM内存溢出的情况:
- 含义:内存溢出。简单来说,就是 JVM 想申请内存来存放对象,但堆内存(Heap)已经满了,且无法再扩展。
- 常见场景:
- 内存泄漏:对象创建了无法被回收
- 大对象:一次性加载了海量数据到内存中
- 解决方案:通常需要优化代码减少内存占用,或调整JVM参数增加内存
StackOverflowError:
- 含义:栈溢出。Java 线程的栈空间是有限的,主要用于存储方法调用的栈帧。
- 常见场景:死递归(无限递归)。方法的循环调用
- 应对:检查代码逻辑,修复递归错误。
2)Exception程序可以捕获处理的异常
受检异常(checked):必须 try-catch 或 throws(如 IOException,SQLException)
非受检异常(unchecked):编译器不强制检查。你可以不处理,程序也能编译运行。如果运行时发生了且没捕获,程序会崩溃并打印堆栈。RuntimeException 子类(如 NPE)
自定义异常:Java 内置的异常虽然丰富,但往往无法精准描述具体的业务场景。比如“用户余额不足”或“密码错误”,用通用的
Exception或RuntimeException显得不够语义化。
# 🟢 5. JVM 运行时数据区(内存模型)
问:JVM 的内存分为哪几个区域?各自的作用是什么?
堆(Heap):所有线程共享,存放对象实例,GC 主要区域
方法区(Method Area):存储类信息、常量、静态变量(JDK8 后用 Metaspace 实现)
虚拟机栈(VM Stack):每个方法创建一个栈帧,存局部变量、操作数栈、动态链接信息、返回地址等
本地方法栈(Native Method Stack):与虚拟机栈作用类似只不过操作的是C\C++语言的方法
程序计数器(PC Register):记录当前线程执行的字节码行号
# 🟡 6. 判断垃圾对象的方式
问:JVM 如何判断一个对象是否可以被回收?引用计数法有什么缺陷?
引用计数法和可达性分析
引用分析法每次被引用计数器+1,引用失效计数器-1
可达性分析:从 GC Roots 出发,不可达的对象可回收
GC Roots:虚拟机栈中的引用对象、方法区中的静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象
引用计数法的缺陷:无法解决循环引用(A 引用 B,B 引用 A,但都不被外部引用)
# 🟢7. 常见的垃圾回收算法
问:你知道哪些垃圾回收算法?分别适用于什么场景?
- 标记-清除:实现简单,容器产生内存碎片,早期JVM
- 标记-复制:浪费一半的空间,不会产生内存碎片,新生代朝生夕死
- 标记-整理:效率低,不会产生内存碎片,老年代内存清理
# 🟡 8. synchronized 和 ReentrantLock 的区别
问:你如何选择使用 synchronized 还是 ReentrantLock?
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 用法 | 自动加/释放锁 | 手动加/释放锁 |
| 可中断 | 不支持 | 支持中断,中断时抛异常,lockInterruptibly() |
| 超时机制 | 无超时机制 | tryLock |
| 公平锁(只根据等待时间排队) | 不支持 | 可自选 |
| 指定唤醒线程 | 只支持随机唤醒和全部唤醒(惊群效应) | 支持 |
需要可中断锁、加超时机制、公平锁、指定唤醒线程时的业务用ReentrantLock,其余简单实现用synchronized
# 🟢 9. 线程池的核心参数
问:ThreadPoolExecutor 的 7 个核心参数分别是什么?任务是如何执行的?
核心线程数、最大线程数、空闲线程存活时间、时间单位、阻塞队列、拒绝策略、线程工厂
1)如果线程<核心线程,先创建线程
2)核心线程满 , 任务入队列
3)任务队列满,创建非核心线程
4)最大线程也满,执行拒绝策略
# 🟡10. volatile 的作用与原理
问:volatile 能保证原子性吗?说说它的内存语义和使用场景。
作用:
1)保证编译的有序性(内存屏障)
2)实时更新数据,强制更新缓存
不能保证原子性,比如i++场景,他相当于三次操作,
- 读取
i的值 - 对
i加 1 - 将新值写回
i
不能保证他的原子性
volatile,它的内存语义就是:“我一旦写入这个值,所有人必须立刻看到新值;我读取这个值时,必须拿到最新的那个。” 同时,它还通过内存屏障禁止了指令重排序,保证了有序性。
使用场景:状态标志位、双重检查锁定单例模式
# 🟢 11. InnoDB 的索引结构
问:InnoDB 为什么使用 B+ 树作为索引结构?和 B 树、Hash 比有什么优势?
相比于B树
- B+树将所有数据都存在叶子节点里,而不是存在每个结点,树更低,IO更少
- 叶子结点的链表支持范围查询
相比于Hash
由于会产生hash冲突形成链表,查询性能不稳定
Hash 适合等值查询,但不支持范围查询和排序
# 🟡 12. 事务隔离级别与问题
问:MySQL 的四种隔离级别分别解决了哪些并发问题?默认级别是什么?
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(RU) | √ | √ | √ |
| 读已提交 (RC) | × | √ | √ |
| 可重复读 (RR) | × | × | √ |
| 线性 | × | × | × |
脏读:读到未提交的数据
不可重复读:同一事务两次读取结果不一致(update)
幻读:两次读取行数不一致(insert)
InnoDB 通过 MVCC + Next-Key Lock 解决了幻读
临建锁:既锁住已有行不让改,又锁住间隙不让插,从根源解决幻读。
# 🟢 13. InnoDB 锁机制
问:说说行锁、间隙锁、临键锁的区别和使用场景。
行锁:锁定单行记录,基于索引实现
间隙锁:锁定索引记录之间的间隙,防止幻读(可重复读RR 级别下)
临键锁 = 行锁 + 间隙锁,锁定一个范围
示例:
SELECT * FROM t WHERE id BETWEEN 1 AND 10 FOR UPDATE→ 锁住 1~10 的间隙