HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

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 堆内存]
    
  • 对象分配流程:
    1. 新对象优先在 Eden 区分配
    2. Eden 满时触发 Minor GC,存活对象进入 S0
    3. 下次 GC 时,Eden + S0 存活对象进入 S1(From/To 交换)
    4. 对象年龄达到 15 次(默认)晋升老年代
    5. 大对象(超过 -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 核心特点:

  1. 分区设计:堆划分为多个 Region(1-32MB),不再严格分代
  2. 可预测停顿:优先回收价值最大的 Region(Garbage-First)
  3. 并发标记:使用 SATB(Snapshot-At-The-Beginning)算法
  4. 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、事务、循环依赖
性能优化案例、高频面试题

高频场景题

  1. 如何排查线上 OOM?
  2. 如何优化高并发场景下的锁竞争?
  3. 如何设计一个本地缓存?
  4. 如何保证分布式事务一致性?
  5. 如何优化慢 SQL?

每个场景都要准备:问题分析 → 解决方案 → 实际效果