Java 面试宝典
从 JVM 底层到并发编程,从集合框架到性能优化,系统掌握 Java 核心技术
一、JVM 与内存管理(高频必考)
1.1 JVM 内存结构详解
场景:面试官问"说说 JVM 内存区域划分"
JVM 内存分为以下五大区域:
(1) 程序计数器(线程私有)
- 作用:记录当前线程执行的字节码指令地址
- 特点:唯一不会 OOM 的区域
- 场景:多线程切换时恢复执行位置
(2) 虚拟机栈(线程私有)
- 作用:存储局部变量表、操作数栈、动态链接、方法出口
- 栈帧结构:
public int calculate(int a, int b) { int result = a + b; // 局部变量存在栈帧中 return result; } // 每次方法调用创建一个栈帧 // 栈帧包含:局部变量(a, b, result)、返回值、操作数栈 - 异常:
- StackOverflowError:递归调用过深
- OutOfMemoryError:线程过多导致无法创建新栈
(3) 本地方法栈
- 作用:为 Native 方法服务
- 示例:Thread.currentThread()、System.arraycopy()
(4) 堆(线程共享,重点)
- 作用:存储所有对象实例和数组
- 分代结构:
堆内存 ├── 新生代(Young Generation) [1/3 堆内存] │ ├── Eden 区(80%) │ └── Survivor 区(20%) │ ├── S0(From,10%) │ └── S1(To,10%) └── 老年代(Old Generation) [2/3 堆内存] - 对象分配流程:
- 新对象优先在 Eden 区分配
- Eden 满时触发 Minor GC,存活对象进入 S0
- 下次 GC 时,Eden + S0 存活对象进入 S1(From/To 交换)
- 对象年龄达到 15 次(默认)晋升老年代
- 大对象(超过 -XX:PretenureSizeThreshold)直接进老年代
(5) 方法区/元空间(线程共享)
- JDK 7:永久代(PermGen),存储类信息、常量池、静态变量
- JDK 8+:元空间(Metaspace),使用本地内存,不再受固定大小限制
- 存储内容:
- 类的元数据(Class 对象)
- 运行时常量池
- 静态变量(JDK 7+移到堆中)
面试追问:为什么要分代?
答:基于"弱分代假说":
1. 大部分对象朝生夕死(98% 的对象生命周期很短)
2. 熬过多次 GC 的对象会存活很久
3. 分代后可以对不同区域使用不同 GC 策略,提高效率
1.2 垃圾回收算法
(1) 标记-清除(Mark-Sweep)
执行过程:
1. 标记阶段:从 GC Roots 开始遍历,标记所有可达对象
2. 清除阶段:回收未被标记的对象
优点:简单,适合老年代
缺点:产生内存碎片,导致大对象无法分配
(2) 复制算法(Copying)
执行过程:
1. 将内存分为两块(From/To)
2. 分配对象只在 From 区
3. GC 时将 From 区存活对象复制到 To 区
4. 清空 From 区,交换 From 和 To
优点:无碎片,效率高
缺点:浪费 50% 内存(优化:Eden:S0:S1 = 8:1:1,浪费 10%)
应用:新生代(对象存活率低)
(3) 标记-整理(Mark-Compact)
执行过程:
1. 标记阶段:标记存活对象
2. 整理阶段:将存活对象移到内存一端
3. 清除边界外的内存
优点:无碎片,不浪费内存
缺点:移动对象开销大,需要 STW(Stop-The-World)
应用:老年代(对象存活率高)
1.3 垃圾收集器对比
G1 收集器(重点)
场景:面试官问"为什么选择 G1?G1 的优势是什么?"
// JVM 参数配置
-XX:+UseG1GC // 启用 G1
-XX:MaxGCPauseMillis=200 // 最大停顿时间 200ms
-XX:G1HeapRegionSize=16m // Region 大小
-XX:InitiatingHeapOccupancyPercent=45 // 堆占用 45% 触发并发标记
G1 核心特点:
- 分区设计:堆划分为多个 Region(1-32MB),不再严格分代
- 可预测停顿:优先回收价值最大的 Region(Garbage-First)
- 并发标记:使用 SATB(Snapshot-At-The-Beginning)算法
- Mixed GC:同时回收新生代和部分老年代
G1 GC 过程:
1. Young GC:回收 Eden + Survivor 区(STW 10-100ms)
2. 并发标记周期:
- 初始标记(STW):标记 GC Roots 直接可达对象
- 并发标记:遍历对象图
- 最终标记(STW):处理 SATB 缓冲区
- 清理阶段:统计 Region 价值
3. Mixed GC:回收新生代 + 价值高的老年代 Region
实际案例:
// 场景:电商系统,堆内存 8G,要求 GC 停顿 < 200ms
-Xmx8g -Xms8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:+ParallelRefProcEnabled // 并行处理引用
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
实测效果:
- Young GC 频率:10-20s 一次,停顿 20-50ms
- Mixed GC 频率:2-3 分钟一次,停顿 100-180ms
- Full GC:基本不发生
ZGC(JDK 11+)
特点:超低延迟(停顿 < 10ms),支持 TB 级堆
-XX:+UseZGC
-XX:ZCollectionInterval=120 // 每 120s 一次 GC
-XX:ZAllocationSpikeTolerance=5 // 容忍 5 倍分配速率峰值
适用场景:
- 大内存服务(16G+)
- 对延迟极其敏感(金融交易、游戏服务器)
1.4 OOM 问题排查
场景:生产环境突然 OOM,如何定位?
(1) 堆内存溢出
// 异常信息
java.lang.OutOfMemoryError: Java heap space
// 排查步骤
1. 导出堆快照
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
2. 使用 MAT(Memory Analyzer Tool)分析
- 查看 Dominator Tree(支配树)
- 定位占用内存最大的对象
- 查看 GC Roots 引用链
// 常见原因
a) 内存泄漏:
- ThreadLocal 未清理
- 集合类持有大量对象
- 监听器未注销
b) 内存不足:
- 缓存数据过多
- 一次性加载大量数据
- 堆内存设置过小
真实案例:
// 问题代码
public class UserCache {
private static Map<Long, User> cache = new HashMap<>();
public void addUser(User user) {
cache.put(user.getId(), user); // 只增不删,内存泄漏
}
}
// 解决方案 1:使用弱引用
private static Map<Long, WeakReference<User>> cache = new WeakHashMap<>();
// 解决方案 2:使用 Guava Cache,自动过期
LoadingCache<Long, User> cache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<Long, User>() {
public User load(Long key) {
return loadUserFromDB(key);
}
});
(2) 元空间溢出
// 异常信息
java.lang.OutOfMemoryError: Metaspace
// 常见原因
1. 动态生成大量类(CGLib、Groovy、JSP)
2. 元空间设置过小
// 解决方案
-XX:MetaspaceSize=256m // 初始元空间
-XX:MaxMetaspaceSize=512m // 最大元空间
(3) 栈溢出
// 异常信息
java.lang.StackOverflowError
// 常见原因
1. 递归调用无终止条件
2. 递归深度过大
// 问题代码
public int fibonacci(int n) {
return fibonacci(n-1) + fibonacci(n-2); // 无终止条件
}
// 解决方案:尾递归优化或改用循环
public int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
二、并发编程(核心重点)
2.1 Java 内存模型(JMM)
场景:面试官问"volatile 和 synchronized 的区别"
JMM 三大特性
(1) 可见性
// 问题代码
public class VisibilityDemo {
private boolean flag = false; // 主线程写,子线程读不到
public void write() {
flag = true; // CPU 缓存,未刷新到主内存
}
public void read() {
while (!flag) { // 读取 CPU 缓存,不是主内存
// 死循环
}
}
}
// 解决方案 1:volatile
private volatile boolean flag = false;
// 作用:
// - 写操作立即刷新到主内存
// - 读操作从主内存读取,不走 CPU 缓存
// 解决方案 2:synchronized
public synchronized void write() {
flag = true;
}
(2) 原子性
// 问题:i++ 不是原子操作
public class AtomicDemo {
private int count = 0;
public void increment() {
count++; // 分三步:读取、+1、写回
}
}
// 多线程执行结果
线程 A:读取 count=0 → 计算 1 → 写回 1
线程 B:读取 count=0 → 计算 1 → 写回 1
结果:count=1(预期 2)
// 解决方案 1:synchronized
public synchronized void increment() {
count++;
}
// 解决方案 2:AtomicInteger(CAS)
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS 操作,无锁
}
(3) 有序性
// 双重检查锁的问题
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) {
if (instance == null) { // 2
instance = new Singleton(); // 3
}
}
}
return instance;
}
}
// 问题:指令重排导致返回未初始化对象
// instance = new Singleton() 分三步:
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将 instance 指向内存地址
//
// 重排后:1 → 3 → 2
// 线程 A 执行到 3,instance 已不为 null
// 线程 B 判断 instance != null,直接返回未初始化对象
// 解决方案:加 volatile 禁止重排
private static volatile Singleton instance;
volatile 原理
实现机制:
1. 内存屏障(Memory Barrier)
- LoadLoad:禁止读-读重排
- StoreStore:禁止写-写重排
- LoadStore:禁止读-写重排
- StoreLoad:禁止写-读重排
2. 缓存一致性协议(MESI)
- Modified:缓存被修改,需写回主内存
- Exclusive:缓存独占,未被修改
- Shared:多个 CPU 共享
- Invalid:缓存失效
volatile 写操作:
写变量 → StoreStore 屏障 → 刷新主内存 → StoreLoad 屏障
volatile 读操作:
LoadLoad 屏障 → 读主内存 → LoadStore 屏障
2.2 synchronized 原理
场景:面试官问"synchronized 底层如何实现?锁升级过程?"
(1) 使用方式
// 1. 修饰实例方法:锁是当前对象
public synchronized void method1() {
// 等价于 synchronized(this)
}
// 2. 修饰静态方法:锁是 Class 对象
public static synchronized void method2() {
// 等价于 synchronized(ClassName.class)
}
// 3. 修饰代码块:自定义锁对象
public void method3() {
synchronized (lock) {
// 临界区
}
}
(2) 字节码实现
// 源代码
public void method() {
synchronized (this) {
count++;
}
}
// 字节码
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 获取锁
4: aload_0
5: dup
6: getfield
9: iconst_1
10: iadd
11: putfield
14: aload_1
15: monitorexit // 释放锁
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 异常也要释放锁
22: aload_2
23: athrow
(3) 锁升级过程(重点)
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
对象头结构(64 位 JVM):
| Mark Word(64 bit) | Class Pointer(64 bit) | Array Length(32 bit,可选) |
Mark Word 存储:
- 锁状态标志位
- 哈希码
- GC 分代年龄
- 线程 ID(偏向锁)
- 指向栈中锁记录的指针(轻量级锁)
- 指向 Monitor 的指针(重量级锁)
锁升级详解:
// 1. 偏向锁(Biased Locking)
场景:只有一个线程访问同步代码块
原理:
- 对象头记录线程 ID
- 下次该线程进入,直接获取锁,无需 CAS
- 适合单线程重入场景
触发升级:
- 其他线程尝试获取锁 → 升级为轻量级锁
// 2. 轻量级锁(Lightweight Locking)
场景:多个线程交替执行,无实际竞争
原理:
- 线程在栈帧中创建锁记录(Lock Record)
- CAS 将对象头的 Mark Word 替换为锁记录指针
- 成功则获取锁,失败则自旋
自旋过程:
while (!CAS(lockRecord)) {
// 自旋等待(默认 10 次)
// JDK 6+ 自适应自旋
}
触发升级:
- 自旋超过阈值
- 线程数超过 CPU 核心数 / 2
→ 升级为重量级锁
// 3. 重量级锁(Heavyweight Locking)
场景:多线程实际竞争,自旋无效
原理:
- 使用操作系统互斥量(Mutex)
- 未获取锁的线程进入阻塞状态(BLOCKED)
- 涉及用户态和内核态切换(开销大)
Monitor 结构:
- _owner:持有锁的线程
- _EntryList:等待锁的线程队列
- _WaitSet:调用 wait() 的线程队列
实际案例:
// 高并发计数器
public class Counter {
private int count = 0;
// 错误示例:高竞争下频繁升级重量级锁
public synchronized void increment() {
count++;
}
}
// 优化方案 1:分段锁(类似 ConcurrentHashMap)
public class SegmentCounter {
private final int SEGMENT_COUNT = 16;
private final int[] segments = new int[SEGMENT_COUNT];
private final Object[] locks = new Object[SEGMENT_COUNT];
public SegmentCounter() {
for (int i = 0; i < SEGMENT_COUNT; i++) {
locks[i] = new Object();
}
}
public void increment() {
int index = (int) (Thread.currentThread().getId() % SEGMENT_COUNT);
synchronized (locks[index]) {
segments[index]++;
}
}
public int getCount() {
int sum = 0;
for (int segment : segments) {
sum += segment;
}
return sum;
}
}
// 优化方案 2:LongAdder(性能最优)
private LongAdder count = new LongAdder();
public void increment() {
count.increment();
}
2.3 并发工具类
(1) CountDownLatch
场景:等待多个线程完成后再执行主线程
// 示例:并行查询多个数据源,汇总结果
public class DataAggregator {
public Map<String, Object> queryAll() throws InterruptedException {
Map<String, Object> result = new ConcurrentHashMap<>();
CountDownLatch latch = new CountDownLatch(3);
// 查询 MySQL
new Thread(() -> {
result.put("mysql", queryMySQL());
latch.countDown();
}).start();
// 查询 Redis
new Thread(() -> {
result.put("redis", queryRedis());
latch.countDown();
}).start();
// 查询 ES
new Thread(() -> {
result.put("es", queryES());
latch.countDown();
}).start();
latch.await(5, TimeUnit.SECONDS); // 最多等待 5 秒
return result;
}
}
(2) CyclicBarrier
场景:多个线程互相等待,到达屏障点后一起执行
// 示例:分批处理数据,每批 100 条一起入库
public class BatchProcessor {
private CyclicBarrier barrier = new CyclicBarrier(10, () -> {
// 10 个线程都到达后执行
batchInsert();
});
public void process(Data data) {
addToBuffer(data);
try {
barrier.await(); // 等待其他线程
} catch (Exception e) {
e.printStackTrace();
}
}
}
(3) Semaphore
场景:限流,控制并发访问数量
// 示例:数据库连接池
public class ConnectionPool {
private Semaphore semaphore = new Semaphore(10); // 最多 10 个连接
private List<Connection> connections = new ArrayList<>();
public Connection getConnection() throws InterruptedException {
semaphore.acquire(); // 获取许可
return fetchConnection();
}
public void releaseConnection(Connection conn) {
returnConnection(conn);
semaphore.release(); // 释放许可
}
}
// 限流器
public class RateLimiter {
private Semaphore semaphore;
public RateLimiter(int permitsPerSecond) {
this.semaphore = new Semaphore(permitsPerSecond);
// 每秒补充令牌
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
semaphore.release(permitsPerSecond - semaphore.availablePermits());
}, 1, 1, TimeUnit.SECONDS);
}
public boolean tryAcquire() {
return semaphore.tryAcquire();
}
}
2.4 线程池详解
场景:面试官问"线程池核心参数?拒绝策略?如何合理设置线程数?"
核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
执行流程
1. 提交任务
↓
2. 核心线程数未满?
是 → 创建核心线程执行
否 → 3
↓
3. 队列未满?
是 → 任务入队
否 → 4
↓
4. 最大线程数未满?
是 → 创建非核心线程执行
否 → 5
↓
5. 执行拒绝策略
拒绝策略
// 1. AbortPolicy(默认):抛出异常
new ThreadPoolExecutor.AbortPolicy()
// 抛出 RejectedExecutionException
// 2. CallerRunsPolicy:调用者线程执行
new ThreadPoolExecutor.CallerRunsPolicy()
// 降低任务提交速度,不丢弃任务
// 3. DiscardPolicy:直接丢弃
new ThreadPoolExecutor.DiscardPolicy()
// 4. DiscardOldestPolicy:丢弃最老的任务
new ThreadPoolExecutor.DiscardOldestPolicy()
// 5. 自定义策略
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志 + 入库 + 告警
logger.warn("Task rejected: {}", r);
saveToDatabase(r);
sendAlert();
}
}
线程数设置
// CPU 密集型:线程数 = CPU 核心数 + 1
int cpuCount = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(cpuCount + 1);
// IO 密集型:线程数 = CPU 核心数 * 2 或更多
// 公式:线程数 = CPU 核心数 * (1 + IO 耗时 / CPU 耗时)
// 示例:IO 耗时 100ms,CPU 耗时 10ms
// 线程数 = 8 * (1 + 100/10) = 88
// 实际案例:HTTP 接口调用
ExecutorService executor = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60, TimeUnit.SECONDS, // 空闲 60s 回收
new LinkedBlockingQueue<>(1000), // 队列 1000
new ThreadFactoryBuilder()
.setNameFormat("http-pool-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
实战:动态调整线程池
public class DynamicThreadPool {
private ThreadPoolExecutor executor;
public DynamicThreadPool() {
executor = new ThreadPoolExecutor(
10, 50, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 监控线程池,动态调整
ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1);
monitor.scheduleAtFixedRate(() -> {
int activeCount = executor.getActiveCount();
int poolSize = executor.getPoolSize();
int queueSize = executor.getQueue().size();
// 活跃线程 > 80%,扩容
if (activeCount > poolSize * 0.8) {
executor.setMaximumPoolSize(Math.min(poolSize + 10, 200));
}
// 队列堆积 > 500,扩容
if (queueSize > 500) {
executor.setCorePoolSize(Math.min(executor.getCorePoolSize() + 5, 50));
}
logger.info("ThreadPool: active={}, pool={}, queue={}",
activeCount, poolSize, queueSize);
}, 10, 10, TimeUnit.SECONDS);
}
}
三、集合框架
3.1 HashMap 原理(必考)
场景:面试官问"HashMap 的实现原理?JDK 1.8 做了哪些优化?"
数据结构
JDK 1.7:数组 + 链表
JDK 1.8:数组 + 链表 + 红黑树
Node<K,V>[] table; // 哈希表
当链表长度 ≥ 8 且数组长度 ≥ 64:链表 → 红黑树
当红黑树节点 ≤ 6:红黑树 → 链表
put 方法流程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 1. 计算 hash
static final int hash(Object key) {
int h;
// 高 16 位异或低 16 位,减少碰撞
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 2. putVal 流程
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1) 表为空,初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2) 计算索引:i = (n - 1) & hash
// 等价于 hash % n,但位运算更快
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 无冲突,直接放入
else {
Node<K,V> e; K k;
// 3) 头节点匹配
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4) 红黑树节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 5) 链表节点
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度 ≥ 8,树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 找到相同 key
p = e;
}
}
// 6) key 已存在,更新 value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
// 7) size++,检查是否需要扩容
if (++size > threshold)
resize();
return null;
}
扩容机制
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int newCap = oldCap << 1; // 扩容为 2 倍
// 重新计算阈值:threshold = capacity * loadFactor
int newThr = (int)(newCap * loadFactor); // 默认 0.75
Node<K,V>[] newTab = new Node[newCap];
table = newTab;
// 重新分配节点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e = oldTab[j];
if (e != null) {
oldTab[j] = null;
// 只有一个节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 链表:拆分为两条链表
else {
// JDK 1.8 优化:根据 (e.hash & oldCap) 分组
// 结果为 0:索引不变(loHead)
// 结果为 1:索引 = j + oldCap(hiHead)
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else {
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
return newTab;
}
关键优化点:
1. 为什么容量是 2 的幂次?
- 计算索引:index = hash & (n - 1)
- n = 16(10000),n - 1 = 15(01111)
- 任何数 & 01111 = 取低 4 位,等价于 % 16
- 位运算比取模快
2. 为什么 loadFactor = 0.75?
- 空间利用率和碰撞概率的权衡
- 0.75 时泊松分布,链表长度超过 8 的概率 < 0.00000006
3. JDK 1.8 链表转红黑树的优势?
- 链表查询:O(n)
- 红黑树查询:O(log n)
- 高碰撞时性能提升明显
并发问题
// JDK 1.7 死循环问题
场景:多线程同时扩容
原因:链表采用头插法,扩容时可能形成环
线程 A:扩容中,执行到一半被挂起
原链表:1 → 2 → 3
新链表:1
线程 B:完成扩容
新链表:3 → 2 → 1
线程 A:恢复执行
尝试插入 2 → 1
形成环:1 ↔ 2
解决:
1. 使用 ConcurrentHashMap
2. Collections.synchronizedMap(new HashMap<>())
3.2 ConcurrentHashMap
场景:面试官问"ConcurrentHashMap 如何实现线程安全?JDK 1.8 的优化?"
JDK 1.7:分段锁
结构:Segment[] + HashEntry[]
锁粒度:Segment(默认 16 个)
class Segment<K,V> extends ReentrantLock {
HashEntry<K,V>[] table;
}
put 操作:
1. 计算 segment 索引
2. 获取 segment 的锁
3. 在 segment 内的 table 中插入
并发度:最多 16 个线程同时写(每个 segment 一个线程)
JDK 1.8:CAS + synchronized
结构:Node[] + 链表/红黑树
锁粒度:Node(数组元素)
put 操作:
1. 计算 hash,定位到 Node
2. 如果 Node 为空:CAS 插入
3. 如果 Node 不为空:synchronized 锁住 Node,插入链表/红黑树
// 关键代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 1. 表未初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 桶为空,CAS 插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// 3. 正在扩容,帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 锁住桶,插入链表/红黑树
else {
V oldVal = null;
synchronized (f) { // 锁粒度:单个桶
// 链表插入逻辑
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
get 操作(无锁):
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 头节点匹配
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 红黑树查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 链表遍历
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
// 为什么 get 不用锁?
// 1. Node 的 val 和 next 都是 volatile
// 2. 读到的一定是最新值或旧值,不会读到中间状态
size 计算:
// JDK 1.7:遍历所有 Segment,重试 3 次不一致则加锁
// JDK 1.8:使用 baseCount + CounterCell[]
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
// put 时更新计数
private final void addCount(long x, int check) {
// 尝试更新 baseCount
if (CAS(baseCount, baseCount + x)) {
return;
}
// 失败则使用 CounterCell(类似 LongAdder)
CounterCell[] as;
if ((as = counterCells) != null) {
int index = ThreadLocalRandom.current().nextInt(as.length);
as[index].value += x;
}
}
// 计算 size
public long size() {
long sum = baseCount;
if (counterCells != null) {
for (CounterCell cell : counterCells) {
sum += cell.value;
}
}
return sum;
}
四、Spring 框架
4.1 IOC 容器
场景:面试官问"Spring IOC 的实现原理?Bean 的生命周期?"
Bean 生命周期
完整流程:
1. 实例化(Instantiation)
2. 属性赋值(Populate)
3. 初始化(Initialization)
- Aware 接口回调
- BeanPostProcessor.postProcessBeforeInitialization()
- InitializingBean.afterPropertiesSet()
- 自定义 init-method
- BeanPostProcessor.postProcessAfterInitialization()
4. 使用(In Use)
5. 销毁(Destruction)
- DisposableBean.destroy()
- 自定义 destroy-method
// 示例代码
@Component
public class UserService implements BeanNameAware, InitializingBean, DisposableBean {
private UserRepository userRepository;
// 1. 构造函数:实例化
public UserService() {
System.out.println("1. 构造函数");
}
// 2. 依赖注入:属性赋值
@Autowired
public void setUserRepository(UserRepository userRepository) {
System.out.println("2. 属性赋值");
this.userRepository = userRepository;
}
// 3. Aware 回调
@Override
public void setBeanName(String name) {
System.out.println("3. BeanNameAware: " + name);
}
// 4. BeanPostProcessor 前置处理
// (由 Spring 容器自动调用)
// 5. InitializingBean
@Override
public void afterPropertiesSet() {
System.out.println("5. InitializingBean.afterPropertiesSet()");
}
// 6. 自定义初始化方法
@PostConstruct
public void init() {
System.out.println("6. @PostConstruct");
}
// 7. BeanPostProcessor 后置处理
// (AOP 代理在这里生成)
// 8. 销毁
@PreDestroy
public void preDestroy() {
System.out.println("8. @PreDestroy");
}
@Override
public void destroy() {
System.out.println("9. DisposableBean.destroy()");
}
}
循环依赖解决
// 场景
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
// 三级缓存机制
public class DefaultSingletonBeanRegistry {
// 一级缓存:成品 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:半成品 Bean(已实例化,未初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:Bean 工厂(用于提前暴露 AOP 代理)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
// 创建流程
1. 创建 A:
- 实例化 A,放入三级缓存
- 填充属性,发现依赖 B
2. 创建 B:
- 实例化 B,放入三级缓存
- 填充属性,发现依赖 A
3. 获取 A:
- 一级缓存没有
- 二级缓存没有
- 三级缓存有,调用 ObjectFactory 创建 A 的早期引用
- 将 A 的早期引用放入二级缓存
- 返回 A 的早期引用给 B
4. B 初始化完成:
- B 放入一级缓存
5. A 继续初始化:
- A 放入一级缓存
// 为什么需要三级缓存?
// 二级缓存足够解决循环依赖,但 AOP 需要三级缓存
// 三级缓存存储 ObjectFactory,用于延迟创建代理对象
// 如果 A 需要代理,ObjectFactory.getObject() 返回代理对象
构造器循环依赖无法解决:
@Service
public class A {
public A(B b) {} // 构造器注入
}
@Service
public class B {
public B(A a) {} // 构造器注入
}
// 报错:BeanCurrentlyInCreationException
// 原因:实例化前无法提前暴露引用
// 解决方案
1. 改为 @Autowired 字段注入或 Setter 注入
2. 使用 @Lazy 延迟加载
@Service
public class A {
public A(@Lazy B b) {}
}
4.2 AOP 原理
场景:面试官问"Spring AOP 的实现方式?动态代理的两种方式?"
JDK 动态代理
// 只能代理接口
public interface UserService {
void save(User user);
}
@Service
public class UserServiceImpl implements UserService {
public void save(User user) {
System.out.println("保存用户:" + user);
}
}
// 代理类
public class JdkProxyFactory implements InvocationHandler {
private Object target;
public Object createProxy(Object target) {
this.target = target;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置通知");
Object result = method.invoke(target, args);
System.out.println("后置通知");
return result;
}
}
// 使用
UserService target = new UserServiceImpl();
UserService proxy = (UserService) new JdkProxyFactory().createProxy(target);
proxy.save(new User());
CGLIB 动态代理
// 可以代理类(通过继承)
@Service
public class UserService { // 没有接口
public void save(User user) {
System.out.println("保存用户:" + user);
}
}
// 代理类
public class CglibProxyFactory implements MethodInterceptor {
public Object createProxy(Class<?> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
System.out.println("前置通知");
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
System.out.println("后置通知");
return result;
}
}
// 使用
UserService proxy = (UserService) new CglibProxyFactory().createProxy(UserService.class);
proxy.save(new User());
Spring AOP 选择策略:
1. 目标对象实现了接口:默认使用 JDK 动态代理
2. 目标对象未实现接口:使用 CGLIB 代理
3. 强制使用 CGLIB:
@EnableAspectJAutoProxy(proxyTargetClass = true)
事务失效场景
// 场景 1:方法不是 public
@Transactional
private void save() {} // 事务不生效
// 原因:Spring AOP 默认只代理 public 方法
// 场景 2:同类方法调用
@Service
public class UserService {
@Transactional
public void save(User user) {
// ...
}
public void batchSave(List<User> users) {
for (User user : users) {
this.save(user); // 事务不生效!
}
}
}
// 原因:this.save() 调用的是原始对象,不是代理对象
// 解决方案 1:注入自己
@Service
public class UserService {
@Autowired
private UserService self; // 注入代理对象
public void batchSave(List<User> users) {
for (User user : users) {
self.save(user); // 调用代理对象
}
}
}
// 解决方案 2:获取代理对象
AopContext.currentProxy()
// 场景 3:异常被捕获
@Transactional
public void save(User user) {
try {
// ...
throw new Exception();
} catch (Exception e) {
// 吞掉异常,事务不回滚
}
}
// 解决:手动回滚
catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
// 场景 4:异常类型不匹配
@Transactional // 默认只回滚 RuntimeException 和 Error
public void save(User user) throws Exception {
throw new Exception(); // 不回滚
}
// 解决:指定异常类型
@Transactional(rollbackFor = Exception.class)
五、性能优化实战
5.1 JVM 调优案例
场景:电商系统,8G 堆内存,频繁 Full GC,接口响应慢
问题排查
# 1. 查看 GC 日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/var/log/gc.log
# GC 日志分析
2024-01-15T10:30:15.123+0800: [Full GC (Ergonomics)
[PSYoungGen: 204800K->0K(307200K)]
[ParOldGen: 6990336K->6990336K(7168000K)]
7195136K->6990336K(7475200K),
[Metaspace: 51234K->51234K(1099776K)], 2.5 seconds]
# 分析:
# - Full GC 耗时 2.5s,严重影响响应
# - 老年代 6990336K ≈ 6.8G,接近满载
# - 回收后老年代仍是 6.8G,说明存在内存泄漏
# 2. dump 堆快照
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
# 3. 使用 MAT 分析
# 发现:某个 Map 占用 5.2G 内存,包含 800 万个 User 对象
# 4. 定位代码
// 问题代码
@Component
public class UserCache {
private Map<Long, User> cache = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 60000)
public void refreshCache() {
List<User> users = userRepository.findAll(); // 查询全量用户
for (User user : users) {
cache.put(user.getId(), user); // 只增不删
}
}
}
解决方案
// 方案 1:使用 Guava Cache,自动过期
@Component
public class UserCache {
private LoadingCache<Long, User> cache = CacheBuilder.newBuilder()
.maximumSize(100000) // 最多缓存 10 万个
.expireAfterWrite(30, TimeUnit.MINUTES) // 30 分钟过期
.build(new CacheLoader<Long, User>() {
public User load(Long id) {
return userRepository.findById(id).orElse(null);
}
});
public User getUser(Long id) {
return cache.getUnchecked(id);
}
}
// 方案 2:使用 Redis 缓存
@Service
public class UserService {
@Autowired
private RedisTemplate<String, User> redisTemplate;
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
}
// 方案 3:分页加载
@Component
public class UserCache {
private Map<Long, User> cache = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 60000)
public void refreshCache() {
int pageSize = 1000;
int pageNo = 0;
Page<User> page;
do {
page = userRepository.findAll(PageRequest.of(pageNo++, pageSize));
for (User user : page.getContent()) {
cache.put(user.getId(), user);
}
} while (page.hasNext());
}
}
JVM 参数优化
# 优化前
-Xms8g -Xmx8g
-XX:+UseParallelGC
# 优化后
-Xms8g -Xmx8g
-XX:+UseG1GC # 使用 G1
-XX:MaxGCPauseMillis=200 # 最大停顿 200ms
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45
-XX:+ParallelRefProcEnabled
-XX:+DisableExplicitGC # 禁止 System.gc()
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
# 效果
- Full GC 频率:每天 10+ 次 → 基本不发生
- GC 停顿时间:2.5s → 100ms 以内
- 接口响应时间:P99 从 3s 降到 200ms
5.2 SQL 优化
场景:订单列表查询慢,2000 万数据,查询耗时 8 秒
问题 SQL
SELECT o.*, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.create_time >= '2024-01-01'
AND o.status = 1
ORDER BY o.create_time DESC
LIMIT 10;
-- 执行计划
EXPLAIN SELECT ...
+----+-------------+-------+------+---------------+------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+----------+-------------+
| 1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 20000000 | Using where |
| 1 | SIMPLE | u | ref | PRIMARY | PRIMARY | 8 | o.user_id | 1 | |
+----+-------------+-------+------+---------------+------+---------+------+----------+-------------+
-- 问题:全表扫描 2000 万行
优化方案
-- 1. 添加索引
CREATE INDEX idx_status_create_time ON orders(status, create_time);
-- 执行计划
+----+-------------+-------+-------+----------------------+----------------------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+----------------------+----------------------+---------+-------+------+-------+
| 1 | SIMPLE | o | range | idx_status_create_time | idx_status_create_time | 9 | NULL | 5000 | |
| 1 | SIMPLE | u | ref | PRIMARY | PRIMARY | 8 | o.user_id | 1 | |
+----+-------------+-------+-------+----------------------+----------------------+---------+-------+------+-------+
-- 优化效果:8s → 200ms
-- 2. 覆盖索引(避免回表)
CREATE INDEX idx_cover ON orders(status, create_time, id, user_id);
SELECT o.id, o.user_id, o.create_time, u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1 AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 10;
-- 优化效果:200ms → 50ms
-- 3. 延迟关联(深分页优化)
-- 问题 SQL:LIMIT 100000, 10 很慢
SELECT o.*, u.username
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1
ORDER BY o.create_time DESC
LIMIT 100000, 10;
-- 优化:先用索引定位 ID,再关联
SELECT o.*, u.username
FROM (
SELECT id FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 100000, 10
) AS tmp
JOIN orders o ON tmp.id = o.id
LEFT JOIN users u ON o.user_id = u.id;
-- 优化效果:5s → 100ms
六、高频面试题速答
6.1 基础篇
Q1:String、StringBuilder、StringBuffer 的区别?
String:
- 不可变,线程安全
- 每次修改都创建新对象
- 适合少量字符串操作
StringBuilder:
- 可变,线程不安全
- 内部使用 char[] 数组
- 适合单线程大量字符串拼接
StringBuffer:
- 可变,线程安全(方法加 synchronized)
- 性能比 StringBuilder 差 10%
- 适合多线程字符串拼接
推荐:优先用 StringBuilder
Q2:== 和 equals 的区别?
==:
- 基本类型:比较值
- 引用类型:比较内存地址
equals:
- Object 默认实现:return this == obj;
- String 重写:比较字符内容
- 自定义对象需重写
// 示例
String s1 = new String("hello");
String s2 = new String("hello");
s1 == s2 // false(不同对象)
s1.equals(s2) // true(内容相同)
// 字符串常量池
String s3 = "hello";
String s4 = "hello";
s3 == s4 // true(指向常量池同一对象)
Q3:重写 equals 为什么要重写 hashCode?
原因:HashMap 等集合依赖 hashCode 定位桶,再用 equals 判断
// 反例
public class User {
private Long id;
@Override
public boolean equals(Object obj) {
return this.id.equals(((User) obj).id);
}
// 未重写 hashCode
}
User u1 = new User(1L);
User u2 = new User(1L);
Map<User, String> map = new HashMap<>();
map.put(u1, "张三");
map.get(u2); // null!
// 原因:u1 和 u2 的 hashCode 不同,定位到不同的桶
// 正确做法
@Override
public int hashCode() {
return Objects.hash(id);
}
6.2 进阶篇
Q4:什么是零拷贝?
传统 IO:
用户空间 ← 内核空间 ← 磁盘(4 次拷贝,2 次上下文切换)
零拷贝(sendfile):
磁盘 → 内核缓冲区 → Socket 缓冲区(2 次拷贝,无用户空间参与)
应用:
- Kafka:使用 sendfile 发送消息
- Netty:FileRegion.transferTo()
- Nginx:sendfile on;
// Java 实现
FileChannel fileChannel = new FileInputStream("file.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open();
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
Q5:分布式锁的实现?
// 方案 1:Redis SETNX
public boolean tryLock(String key, String value, int expireSeconds) {
String result = jedis.set(key, value, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
public void unlock(String key, String value) {
// Lua 脚本保证原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
}
// 方案 2:Redisson(推荐)
RLock lock = redisson.getLock("myLock");
try {
lock.lock(10, TimeUnit.SECONDS); // 自动续期
// 业务逻辑
} finally {
lock.unlock();
}
// 方案 3:ZooKeeper 临时顺序节点
七、面试宝典总结
核心考点分布
JVM 与内存管理:25%
并发编程:30%
集合框架:15%
Spring 框架:20%
性能优化:10%
学习路径
JVM 内存模型、GC 算法、调优工具
第 2-3 周:并发编程(JMM、锁、线程池、并发工具类)
集合框架(HashMap、ConcurrentHashMap 源码)
Spring IOC/AOP、事务、循环依赖
性能优化案例、高频面试题
高频场景题
- 如何排查线上 OOM?
- 如何优化高并发场景下的锁竞争?
- 如何设计一个本地缓存?
- 如何保证分布式事务一致性?
- 如何优化慢 SQL?
每个场景都要准备:问题分析 → 解决方案 → 实际效果