# 02.面试八股
# 🟡 题1:HashMap 的树化与退化
题目:
HashMap 中链表转红黑树的阈值是 8,红黑树退化成链表的阈值是 6,为什么不是 7?
如果 hashCode 分布非常均匀,还会发生树化吗?
8和6之间由7作为缓冲,防止因频繁树化,退化造成性能抖动
树化除了链表长度 ≥ 8 外,还要数组长度 ≥ 64(否则先扩容),hashCode 分布非常均匀时,链表长度几乎不会超过 8,不会树化
# 🔴 题2:String 的“不可变性”真的绝对吗?
题目:
以下代码会输出什么?为什么?
String s1 = "hello";
String s2 = s1;
try {
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(s1);
value[0] = 'H';
} catch (Exception e) {}
System.out.println(s1);
System.out.println(s2);
2
3
4
5
6
7
8
9
10
都输出Hello,String一般是不可变的,但是可以通过反射,修改char数组中的值
s2输出Hello是因为,他们都指向相同的堆内存引用
# 🟢 题3:异常处理中的 finally 与 return
题目:
以下代码输出什么?
public static int test() {
int x = 1;
try {
return x;
} finally {
x = 2;
}
}
System.out.println(test());
2
3
4
5
6
7
8
9
1,已经返回了,最后给x赋值影响不了什么
# 🟡 题4:对象一定在堆上分配吗?
题目:
哪些情况下对象不直接在堆上分配?说出 2 种以上。
栈上分配(标量替换):
- 逃逸分析 + JIT 确定对象不逃逸 → 拆散为变量存在栈帧
| 逃逸程度 | 描述 | 分配策略 |
|---|---|---|
| 不逃逸 (No Escape) | 对象仅在方法内部使用,不会被外部引用。 | 栈上分配 或 标量替换 |
| 方法逃逸 (Method Escape) | 对象被返回,或被传递给其他方法,但未跨线程。 | 通常在堆上分配(可能尝试优化) |
| 线程逃逸 (Thread Escape) | 对象被赋值给静态变量,或被其他线程访问。 | 必须在堆上分配 |
public void testNoEscape() {
// 对象仅在方法内使用,未返回,未赋值给外部变量
User user = new User("Alice", 18);
System.out.println(user.getName());
}
2
3
4
5
# 🟢 题5:Full GC 频繁的排查思路
题目:
线上系统频繁 Full GC,你作为实习生接到任务排查,给出完整排查步骤。
jstat -gcutil <pid> 1s看老年代、Eden、GC 次数jmap -histo <pid> | head -20看大对象、内存泄漏嫌疑可疑时
jmap -dump:live,format=b,file=heap.hprof <pid>用 MAT / JProfiler 分析 dump(重点看 GC Roots、大对象引用链)
| 故障类型 | 典型特征 | 常见原因 | 解决方案 |
|---|---|---|---|
| 内存泄漏 | Full GC 后老年代占用率不下降,呈阶梯状上涨 | 1. 静态集合(static Map/List)无限添加不清理2. ThreadLocal 未 remove3. 未关闭的资源(IO流、DB连接) 4. 监听器/回调未注销 | 1. 使用 MAT 定位泄漏对象 2. 改用 WeakHashMap 或带过期策略的缓存(Caffeine)3. 规范资源关闭(try-with-resources) |
| 大对象过早晋升 | 老年代占用高,但无泄漏,Full GC 后能回收一部分 | 1. 代码中一次性加载大量数据(如全表查询) 2. 超大字符串或 byte 数组 3. 频繁的大对象分配直接进入老年代 | 1. 优化 SQL,使用分页查询 2. 检查代码中的大对象创建 3. 调整 -XX:PretenureSizeThreshold(仅限 Serial/ParNew) |
| 元空间不足 | GC 日志显示 Metadata GC Threshold,Metaspace 持续增长 | 1. 大量使用 CGLIB/Dynamic Proxy 2. 频繁的热部署或动态加载类 3. JSON 序列化框架生成大量类 | 1. 适当调大 -XX:MaxMetaspaceSize2. 优化代码减少动态类生成 |
| 堆配置不合理 | 频繁 Full GC,但每次回收效果尚可,只是空间太小 | 1. 新生代(Young Gen)太小,对象过早进入老年代 2. Survivor 区太小,对象提前晋升 | 1. 调整 -Xmn(新生代大小)2. 调整 -XX:SurvivorRatio3. 调大堆内存 -Xmx |
| 显式 GC | GC 日志明确记录 System.gc() | 1. 代码中调用了 System.gc()2. 使用了 RMI 或 NIO 的 DirectByteBuffer 自动清理机制 | 1. 搜索代码并删除 2. 启动参数添加 -XX:+DisableExplicitGC |
# 🔴 题6:类加载的“死锁”场景
题目:
两个自定义类加载器相互引用对方加载的类,会不会死锁?为什么?
“通常情况下不会死锁。
JVM 机制保护:JVM 在类加载和链接阶段有内置的状态检查机制。如果检测到类的继承链或依赖链存在循环(例如 A 依赖 B,B 依赖 A),JVM 不会让线程无限阻塞,而是会抛出 ClassCircularityError。
# 🟡 题7:synchronized 的锁升级过程
题目:
synchronized 在 JDK 1.6 后做了哪些优化?无锁 → 偏向锁 → 轻量级锁 → 重量级锁分别在什么条件下发生?
在JDK1.6有了锁升级机制,会根据系统中的需要对锁进行升级
当最初创建时为无锁状态,对象未被任何线程访问,当有一个线程访问后进入偏向锁的状态,当有第二个线程进行访问时进入轻量级锁状态,第二个线程如果获取不到锁,不会进入阻塞状态而是进行CAS+自旋(自己调整循环次数,占用CPU,不切换上下文),当第二个线程一直获取不到锁或者系统并发比较高时,进入重量级锁状态,获取不到锁的线程会被放到队列中,等待操作系统来唤醒,涉及到上下文的切换,开销比较大
| 锁状态 | 触发条件 | 核心机制 | 性能开销 |
|---|---|---|---|
| 偏向锁 | 只有1个线程反复访问 | 记录线程ID,无同步操作 | 极低 (几乎无开销) |
| 轻量级锁 | 有少量线程竞争,但时间短 | CAS + 自旋 (占用CPU但不阻塞) | 较小 (避免内核切换) |
| 重量级锁 | 激烈竞争,自旋失败 | 操作系统互斥量,线程阻塞 | 极大 (涉及内核切换) |
# 🔴题8:线程池中的“工作窃取”
题目:
ForkJoinPool 的核心机制是什么?和普通 ThreadPoolExecutor 最大的区别在哪里?
核心机制:工作窃取
区别:
ThreadPoolExecutor:一个全局的共享队列
ForkJoinPool:每个线程有自己的双端队列,空闲线程从其他线程队尾“偷”任务
适用场景:分治任务(递归拆分 + 合并结果)
# 🟢 题9:volatile + 双重检查锁的坑
题目:
以下 DCL 单例有什么问题?
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
2
3
4
5
6
7
指令重排序问题:
new Singleton()不是原子操作分配内存
初始化对象
instance 指向内存 ← 2 和 3 可能重排
其他线程可能拿到未初始化完成的对象
修复:
private static volatile Singleton instance
# 🟡 题10:一个 SQL 不走索引的真实案例
题目:
表 user(name, age),联合索引 (name, age),以下 SQL 是否走索引?为什么?
sql
SELECT * FROM user WHERE age = 20 AND name LIKE '%张%';
不会走联合索引
原因:
LIKE '%张%'是前缀模糊匹配,无法使用索引即使条件顺序调换也没用
优化方向:
name LIKE '张%'(前缀匹配可用索引)或走全文索引
# 🔴 题11:MVCC 与 Read View
题目:
在 RR 隔离级别下,一个事务开启后一直不提交,另一个事务 insert 一条新数据,第一个事务能否读到?为什么?
MVCC 中 Read View 在第一个 SELECT 时创建(不是事务开始时)
| 场景 | 操作顺序 | 结果 | 原因 |
|---|---|---|---|
| 场景一 | 事务1查询 → 事务2插入提交 → 事务1再查询 | 查不到 | 事务1在事务2插入之前就创建了快照,快照里不包含新数据。 |
| 场景二 | 事务2插入提交 → 事务1首次查询 | 能查到 | 事务1在事务2提交之后才创建快照,新数据对快照可见。 |
# 🟢 题12:锁的“自查询”场景
题目:
以下 SQL 在 RR 隔离级别下会锁哪些行?
SELECT * FROM orders WHERE id > 10 FOR UPDATE;
表中 id 为主键,值:5, 8, 11, 15, 20
锁定的行:11, 15, 20
同时锁定 id 10 到正无穷的间隙(包括 id=20 之后的无限)
常见错误:认为会锁 5, 8(不会,因为 id ≤ 10 的行不在范围)