# 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++场景,他相当于三次操作,

  1. 读取 i 的值
  2. 对 i 加 1
  3. 将新值写回 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 的间隙

最近更新: 9/19/2026, 1:27:08 PM
编程NOTE   |