2026求职旺季Java面试必杀技:让面试官刮目相看的深度技术指南

0 阅读26分钟

2026Java面试必杀技:让面试官刮目相看的深度技术指南

🎯 写在前面:又到了一年一度的求职旺季,你是否还在为面试发愁?面对面试官的灵魂拷问,你是否能从容应对?这篇文章,将帮你把Java核心技术讲透,让你在面试中脱颖而出!


前言

又到了一年一度的求职旺季。作为一名Java开发者,你是否也有这样的烦恼:

"HashMap的底层结构是什么?"

"synchronized和ReentrantLock有什么区别?"

"JVM调优怎么做?"

"MySQL索引为什么选择B+树?"

"Redis分布式锁怎么实现?"

这些问题几乎是Java面试的"八股文",但你知道面试官真正想听到的是什么吗?

不是背答案,而是理解原理!

今天这篇文章,我将从高频面试题出发,深入剖析背后的技术原理,让你在面试中不仅能答出来,还能答出深度,让面试官刮目相看!


一、Java基础:从入门到精通

1.1 String为什么是不可变的?

这是一道经典的Java基础面试题,看似简单,实则暗藏玄机。

表面回答:

// String类的核心实现
public final class String {
    private final char[] value;  // JDK 8
    // private final byte[] value;  // JDK 9+ 使用byte数组更节省内存
    
    public String(String original) {
        this.value = original.value;
    }
    // ...
}

深层理解:

┌─────────────────────────────────────────────────────────────────────┐
│                      String不可变的秘密                              │
│                                                                      │
│   为什么String要设计成不可变?                                        │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    五大原因                                   │   │
│   │                                                              │   │
│   │   1️⃣ 字符串常量池(String Pool)                            │   │
│   │      String s1 = "hello";  // 直接从常量池获取                │   │
│   │      String s2 = "hello";  // 同一个引用!                   │   │
│   │      如果可变,s1修改会影响s2                                 │   │
│   │                                                              │   │
│   │   2️⃣ 安全性                                                 │   │
│   │      数据库连接、文件路径、用户名密码...                     │   │
│   │      如果可变,黑客可以轻易篡改!                             │   │
│   │                                                              │   │
│   │   3️⃣ 线程安全                                                │   │
│   │      不可变对象天然线程安全,无需同步                         │   │
│   │                                                              │   │
│   │   4️⃣ 哈希码缓存                                              │   │
│   │      String的hashCode只计算一次,之后缓存                    │   │
│   │      public int hashCode() {                                │   │
│   │          int h = hash;  // 只初始化一次                      │   │
│   │          return h;                                           │   │
│   │      }                                                        │   │
│   │                                                              │   │
│   │   5️⃣ 类加载器机制                                            │   │
│   │      类的加载依赖字符串不可变,否则可能导致类加载错误        │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

面试加分回答:

面试官问:"String真的完全不可变吗?"
高情商回答:"从设计层面看,String通过final修饰类、private修饰字段、且不提供setter方法实现了不可变。
但Java的不可变性并非绝对,通过反射可以绕过封装:
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);
    valueField.set(str, new char[]{'a','b','c'});
不过这是破坏性操作,生产环境绝不能这样用。"

1.2 HashMap源码深度解析

HashMap绝对是Java面试的"钉子户",必问无疑!

JDK 8数据结构演进:

┌─────────────────────────────────────────────────────────────────────┐
│                      HashMap数据结构演进                            │
│                                                                      │
│   JDK 7:数组 + 链表                                                  │
│   ┌─────┬─────┬─────┬─────┬─────┬─────┐                            │
│   │ [0][1][2][3][4][5] │  ← 数组(桶)                │
│   └──┬──┴──┬──┴──┬──┴─────┴──┬──┴─────┘                            │
│      │     │     │           │                                     │
│      ▼     ▼     ▼           ▼                                     │
│    ┌───┐ ┌───┐ ┌───┐       ┌───┐                                 │
│    │ K │→│ K │→│ K │        │ K │  ← 链表                          │
│    │ V │ │ V │ │ V │        │ V │                                 │
│    └───┘ └───┘ └───┘       └───┘                                 │
│                                                                      │
│   问题:链表过长时,查找效率退化到O(n)                                │
│                                                                      │
│   JDK 8:数组 + 链表 + 红黑树(桶内链表长度≥8时)                     │
│   ┌─────┬─────┬─────┬─────┬─────┬─────┐                            │
│   │ [0][1][2][3][4][5] │  ← 数组(桶)                │
│   └──┬──┴──┬──┴──┬──┴─────┴──┬──┴─────┘                            │
│      │     │     │           │                                     │
│      ▼     ▼     ▼           ▼                                     │
│    ┌───┐ ┌───┐  红黑树     ┌───┐                                 │
│    │ K │→│ K │    ┃         │ K │                                 │
│    │ V │ │ V │    ┃         │ V │                                 │
│    └───┘ └───┘    ┃         └───┘                                 │
│                    ▼                                                │
│                  ┌─────┐                                           │
│                  │ BST │  ← 红黑树,查找O(logn)                     │
│                  └─────┘                                           │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

put()方法源码解析:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * 核心put方法
 */
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️⃣ 首次put时,初始化数组(懒加载)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 2️⃣ 计算下标,如果该位置为空,直接放入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 3️⃣ 该位置已有元素(碰撞)
        Node<K,V> e; K k;
        
        // 4️⃣ key相同,覆盖value
        if (p.hash == hash &&
            ((k = p.key) == key || key.equals(k)))
            e = p;
        // 5️⃣ 是红黑树节点,调用树插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>) p).putTreeVal(this, tab, hash, key, value);
        else {
            // 6️⃣ 链表遍历
            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;
                }
                
                // key已存在,覆盖
                if (e.hash == hash &&
                    ((k = e.key) == key || key.equals(k)))
                    break;
                
                p = e;
            }
        }
        
        // value覆盖
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    ++modCount;
    
    // 7️⃣ 元素数量超过阈值,扩容
    if (++size > threshold)
        resize();
    
    afterNodeInsertion(evict);
    return null;
}

扩容机制:

/**
 * 扩容机制:2倍扩容,重新hash
 * 
 * 关键点:扩容时为什么不需要重新计算hash?
 * 答案:因为容量始终是2的n次方,新位置 = 原位置 + 原容量
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    // 原容量 > 0
    if (oldCap > 0) {
        // 超过最大容量,不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新容量 = 原容量 * 2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    // ...
    
    // 创建新数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    // 迁移元素
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != 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 {
                    // 链表分裂:不需要重新hash
                    // 原位置节点 和 原位置+旧容量节点
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    
                    do {
                        next = e.next;
                        // 根据hash & oldCap判断
                        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;
}

面试高频追问:

Q1: HashMap为什么线程不安全?
A1: 
    1. 多线程put可能导致数据覆盖
    2. resize时可能形成环形链表,导致死循环
    3. JDK 7头插法:并发时可能把链表顺序搞反
    4. JDK 8尾插法:解决了顺序问题,但仍可能丢数据

Q2: ConcurrentHashMap如何保证线程安全?
A2: 
    JDK 7:Segment分段锁
    JDK 8:CAS + synchronized,锁住单个bucket
    分段锁 → 锁住数组某个节点

Q3: 为什么HashMap的负载因子是0.75?
A3: 
    统计学原理:泊松分布,链表长度超过8的概率已经很小
    空间和时间权衡:太大会增加碰撞,太小会浪费空间

二、并发编程:硬核知识

2.1 synchronized关键字深度解析

synchronized是Java中最常见的同步手段,但你真的理解它吗?

对象锁的monitor机制:

┌─────────────────────────────────────────────────────────────────────┐
│                      synchronized原理                               │
│                                                                      │
│   每个对象都有一个monitor(监视器锁)                                 │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    对象内存结构                              │   │
│   │                                                              │   │
│   │   对象头(Object Header)                                    │   │
│   │   ┌─────────────────────────────────────────────────────┐   │   │
│   │   │  Mark Word(标记字段)                                │   │   │
│   │   │  ┌───────────────────────────────────────────────┐  │   │   │
│   │   │  │ 无锁状态 │ 偏向锁 │ 轻量级锁 │ 重量级锁 │ GC标记 │  │   │   │
│   │   │  │  0101/10001011   │  │   │   │
│   │   │  └───────────────────────────────────────────────┘  │   │   │
│   │   │  Klass Pointer(指向类元数据)                       │   │   │
│   │   │  实例数据(Instance Data)                           │   │   │
│   │   └─────────────────────────────────────────────────────┘   │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   锁的升级过程(不可逆):                                            │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                              │   │
│   │   无锁 ────→ 偏向锁 ────→ 轻量级锁 ────→ 重量级锁            │   │
│   │   (01)      (01/10)      (00)           (10)               │   │
│   │     │          │            │              │                │   │
│   │     │          │            │              │                │   │
│   │     │     第一次访问     竞争出现       竞争激烈              │   │
│   │     │       加锁         CAS失败         自旋超过阈值        │   │
│   │                                                              │   │
│   │   升级时机:                                                │   │
│   │   1. 偏向锁:只有一个线程访问同步块                         │   │
│   │   2. 轻量级锁:有线程竞争,但很快就能获取                   │   │
│   │   3. 重量级锁:竞争激烈,自旋失败                          │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

synchronized的四种用法:

public class SynchronizedDemo {
    
    // 1️⃣ 修饰代码块:锁定指定对象
    public void method1() {
        synchronized (this) {  // 锁当前实例
            // 临界区
        }
    }
    
    public void method2() {
        synchronized (SynchronizedDemo.class) {  // 锁类对象
            // 临界区
        }
    }
    
    // 2️⃣ 修饰实例方法:锁定当前实例
    public synchronized void method3() {
        // 等价于 synchronized(this)
    }
    
    // 3️⃣ 修饰静态方法:锁定类对象
    public static synchronized void method4() {
        // 等价于 synchronized(SynchronizedDemo.class)
    }
}

synchronized vs ReentrantLock:

import java.util.concurrent.locks.ReentrantLock;

/**
 * synchronized vs ReentrantLock 对比
 */
public class LockComparison {
    
    // synchronized:自动释放锁
    public synchronized void syncMethod() {
        try {
            // 临界区
        } finally {
            // 不需要手动释放,编译器会自动处理
        }
    }
    
    // ReentrantLock:手动释放锁
    private final ReentrantLock lock = new ReentrantLock();
    
    public void lockMethod() {
        lock.lock();  // 获取锁
        try {
            // 临界区
        } finally {
            lock.unlock();  // 必须手动释放
        }
    }
    
    // ReentrantLock的高级功能
    public void advancedLockMethod() {
        // 1. 可中断锁
        try {
            lock.lockInterruptibly();
            // 可以被中断的等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 2. 尝试获取锁(非阻塞)
        if (lock.tryLock()) {
            try {
                // 获取成功
            } finally {
                lock.unlock();
            }
        } else {
            // 获取失败
        }
        
        // 3. 尝试获取锁(带超时)
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
                    // 5秒内获取到了锁
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 4. 公平锁
        ReentrantLock fairLock = new ReentrantLock(true);
    }
}
┌─────────────────────────────────────────────────────────────────────┐
│                      synchronized vs ReentrantLock                   │
│                                                                      │
│   ┌────────────────────┬────────────────────┐                        │
│   │    synchronized    │   ReentrantLock   │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    JVM内置         │    JDK显式实现     │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    自动释放        │    手动释放        │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    不可中断        │    可中断          │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    非公平锁        │    支持公平/非公平 │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    粒度粗          │    粒度细          │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    条件锁          │    支持多个条件    │                        │
│   │    (wait/notify)   │    (Condition)     │                        │
│   ├────────────────────┼────────────────────┤                        │
│   │    无法获取锁状态  │    可查询锁状态    │                        │
│   └────────────────────┴────────────────────┘                        │
│                                                                      │
│   选择建议:                                                          │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ✓ 简单同步 → synchronized                                  │   │
│   │  ✓ 需要高级功能 → ReentrantLock                             │   │
│   │  ✓ 高并发性能 → ReentrantLock + 分段锁                      │   │
│   │  ✓ 代码简洁优先 → synchronized                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2.2 ThreadLocal源码解析

ThreadLocal是面试中的"隐藏Boss",看似简单,实则暗藏玄机!

核心原理图解:

┌─────────────────────────────────────────────────────────────────────┐
│                      ThreadLocal原理                                 │
│                                                                      │
│   每个Thread对象都有一个 ThreadLocalMap                              │
│                                                                      │
│   Thread ─────────────────────────────────────────────────────┐     │
│   │                                                           │     │
│   │   ThreadLocal.ThreadLocalMap                             │     │
│   │   ┌───────────────────────────────────────────────────┐  │     │
│   │   │  table[]                                           │  │     │
│   │   │  ┌─────────┬─────────┬─────────┬─────────┐        │  │     │
│   │   │  │ EntryEntryEntry   │ ...     │        │  │     │
│   │   │  │ (k,v)   │ (k,v)   │ (k,v)   │         │        │  │     │
│   │   │  │        │         │         │         │        │  │     │
│   │   │  └─────────┴─────────┴─────────┴─────────┘        │  │     │
│   │   │                                                     │  │     │
│   │   │  Entry结构:                                        │  │     │
│   │   │  ┌─────────────────────────────────────────────┐  │  │     │
│   │   │  │  static class Entry extends WeakReference<           
│   │   │  │          ThreadLocal<?>> {                   │  │  │     │
│   │   │  │      Object value;                           │  │  │     │
│   │   │  │  }                                           │  │  │     │
│   │   │  └─────────────────────────────────────────────┘  │  │     │
│   │   └───────────────────────────────────────────────────┘  │     │
│   │                                                           │     │
│   └───────────────────────────────────────────────────────────┘     │
│                                                                      │
│   ThreadLocal变量本身不存在于线程中,只是一个Key                      │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ThreadLocal<String> tl = new ThreadLocal<>();             │   │
│   │  tl.set("Hello");                                          │   │
│   │                                                             │   │
│   │  实际存储位置:                                              │   │
│   │  Thread.currentThread()                                     │   │
│   │       ↓                                                     │   │
│   │  threadLocals (ThreadLocalMap)                              │   │
│   │       ↓                                                     │   │
│   │  { ThreadLocal对象 → "Hello" }                             │   │
│   │                                                             │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

源码实现:

public class ThreadLocal<T> {
    
    // ThreadLocal的hash值(原子递增)
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    
    // 获取下一个hash值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    // 设置值
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    // 获取值
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T) e.value;
        }
        return setInitialValue();
    }
    
    // 移除值(重要!防止内存泄漏)
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    
    // 创建ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

内存泄漏问题(重点):

/**
 * ThreadLocal内存泄漏的原因:
 * 
 * ThreadLocalMap的Entry继承自WeakReference<ThreadLocal<?>>
 * 这意味着Entry对ThreadLocal对象是弱引用
 * 
 * 但是!Entry的value是强引用!
 */
public class MemoryLeakDemo {
    
    public static void main(String[] args) {
        // 1. 创建ThreadLocal
        ThreadLocal<byte[]> tl = new ThreadLocal<>();
        tl.set(new byte[1024 * 1024 * 10]);  // 10MB
        
        // 2. tl变量被置为null
        tl = null;
        
        // 3. 此时ThreadLocal对象可以被GC回收(弱引用)
        // 但Thread还在,ThreadLocalMap还在,Entry还在
        // Entry.value还持有10MB数据,无法回收!
        
        System.gc();
        
        // 正确的做法:
        // ThreadLocal使用完毕后,手动调用remove()
        ThreadLocal<String> safeTl = new ThreadLocal<>();
        try {
            safeTl.set("some data");
            // 使用...
        } finally {
            safeTl.remove();  // 重要!防止内存泄漏
        }
    }
}
┌─────────────────────────────────────────────────────────────────────┐
│                      ThreadLocal内存泄漏分析                        │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                        引用链                                │   │
│   │                                                              │   │
│   │   ThreadLocal对象 ──弱引用──→ Entry (key)                     │   │
│   │                                  │                           │   │
│   │                                  │ 强引用                     │   │
│   │                                  ▼                            │   │
│   │                              value对象                        │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   场景分析:                                                          │
│                                                                      │
│   情况1:ThreadLocal变量置为null                                      │
│   ThreadLocal对象(key) → 被GC回收(弱引用)                         │
│   但Entry.value仍持有数据,且Thread还在 → 内存泄漏!                  │
│                                                                      │
│   情况2:ThreadLocal.remove()                                          │
│   Entry被显式移除 → value无引用 → 被GC回收 → 安全!                  │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                     最佳实践                                 │   │
│   │                                                              │   │
│   │   try {                                                      │   │
│   │       ThreadLocal<User> userLocal = new ThreadLocal<>();   │   │
│   │       userLocal.set(currentUser);                           │   │
│   │       // 业务逻辑                                            │   │
│   │   } finally {                                                │   │
│   │       userLocal.remove();  // !必须调用                    │   │
│   │   }                                                          │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

三、JVM虚拟机:底层奥秘

3.1 类加载机制深度解析

类加载是JVM的起点,也是面试高频考点。

类加载的七个阶段:

┌─────────────────────────────────────────────────────────────────────┐
│                      类加载生命周期                                  │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                             │   │
│   │   ① 加载 ──→ ② 验证 ──→ ③ 准备 ──→ ④ 解析                  │   │
│   │                          │                                 │   │
│   │                          │                                 │   │
│   │                          ▼                                 │   │
│   │                     ⑤ 初始化                                │   │
│   │                          │                                 │   │
│   │                          │                                 │   │
│   │   ⑦ 卸载 ◀────────────────┘                                 │   │
│   │        │                                                        │   │
│   │        │                                                         │   │
│   │        └──────────────────⑥ 使用                             │   │
│   │                                                                     │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   详细说明:                                                          │
│                                                                      │
│   ① 加载(Loading)                                                  │
│      - 通过类的全限定名获取类的二进制字节流                            │
│      - 将字节流转化为方法区的运行时数据结构                          │
│      - 在堆中生成java.lang.Class对象,作为访问入口                   │
│                                                                      │
│   ② 验证(Verification)                                              │
│      - 文件格式验证(魔数、版本号等)                                 │
│      - 元数据验证(语义分析)                                         │
│      - 字节码验证(数据流、控制流分析)                               │
│      - 符号引用验证(解析阶段)                                       │
│                                                                      │
│   ③ 准备(Preparation)                                               │
│      - 分配内存,初始化静态变量                                       │
│      - 注意:静态变量赋默认值,不是赋值!                             │
│        static int a = 10;  // 准备阶段 a=0,初始化阶段 a=10          │
│                                                                      │
│   ④ 解析(Resolution)                                                │
│      - 符号引用 → 直接引用                                            │
│      - 类/接口、字段、类方法、方法类型等                              │
│                                                                      │
│   ⑤ 初始化(Initialization)                                           │
│      - 执行类构造器<clinit>                                           │
│      - 静态变量赋值、静态代码块执行                                   │
│      - 线程安全,由JVM保证                                            │
│                                                                      │
│   使用/卸载...                                                        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

双亲委派模型:

/**
 * 双亲委派模型
 * 
 * 当类加载器收到加载请求时,先委托给父类加载器处理
 * 只有父类加载器无法完成时,才自己尝试加载
 */
public class ClassLoaderDemo {
    
    // 类加载器层级
    public static void showClassLoaderHierarchy() {
        ClassLoader current = ClassLoaderDemo.class.getClassLoader();
        
        do {
            System.out.println(current);
            current = current.getParent();
        } while (current != null);
        
        // 输出:
        // sun.misc.Launcher$AppClassLoader@xxxxx  (应用类加载器)
        // sun.misc.Launcher$ExtClassLoader@xxxxx  (扩展类加载器)
        // null                               (Bootstrap启动类加载器)
    }
}

/**
 * 双亲委派模型源码
 */
public abstract class ClassLoader {
    
    // 核心方法:loadClass
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        
        synchronized (getClassLoadingLock(name)) {
            // 1️⃣ 检查是否已加载
            Class<?> c = findLoadedClass(name);
            if (c != null) {
                return c;
            }
            
            // 2️⃣ 委托给父类加载器
            try {
                ClassLoader parent = this.getParent();
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 父类为null,说明是BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类找不到,不处理
            }
            
            // 3️⃣ 父类都找不到,自己加载
            if (c == null) {
                c = findClass(name);
            }
            
            return c;
        }
    }
    
    // findClass由子类实现
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}
┌─────────────────────────────────────────────────────────────────────┐
│                      双亲委派模型                                    │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    Bootstrap ClassLoader                     │   │
│   │                    (启动类加载器,C++实现)                    │   │
│   │                    加载 JAVA_HOME/lib 核心类                 │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    Extension ClassLoader                     │   │
│   │                    (扩展类加载器)                              │   │
│   │                    加载 JAVA_HOME/lib/ext 扩展类              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    Application ClassLoader                    │   │
│   │                    (应用类加载器)                              │   │
│   │                    加载 classpath 下的类                      │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   加载过程:                                                          │
│                                                                      │
│   用户想要加载 java.lang.String                                     │
│        │                                                             │
│        ▼                                                             │
│   ApplicationClassLoader.loadClass()                               │
│        │                                                             │
│        ▼                                                             │
│   委托给父类:ExtensionClassLoader.loadClass()                       │
│        │                                                             │
│        ▼                                                             │
│   委托给父类:BootstrapClassLoader.loadClass()                       │
│        │                                                             │
│        ▼                                                             │
│   Bootstrap找到了!返回java.lang.String                             │
│                                                                      │
│   好处:                                                              │
│   ✓ 防止核心类被篡改(安全性)                                       │
│   ✓ 防止类被重复加载(唯一性)                                       │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

3.2 JVM垃圾回收深度解析

GC是JVM最复杂的部分,也是面试最常问的难点!

垃圾回收算法:

/**
 * JVM垃圾回收算法演示
 */
public class GCDemo {
    
    public static void main(String[] args) {
        
        // ========== 1. 引用计数法(已弃用) ==========
        // 问题:循环引用无法回收
        // A引用B,B引用A,但它们都不再被其他对象引用
        // 引用计数都是1,无法回收!
        
        // ========== 2. 可达性分析算法 ==========
        // GC Roots 向下搜索,搜索路径叫"引用链"
        // 不在引用链中的对象 = 垃圾
        
        // GC Roots包括:
        // - 虚拟机栈(栈帧中的本地变量表)中引用的对象
        // - 方法区中类静态属性引用的对象
        // - 方法区中常量引用的对象
        // - 本地方法栈中JNI引用的对象
        // - Java虚拟机内部的引用(Class对象、异常对象等)
        // - 所有被同步锁(synchronized)持有的对象
        // - 反映Java虚拟机内部情况的JMXBean、回调类、缓存类等
        
        // ========== 3. 标记-清除算法 ==========
        // 缺点:产生内存碎片
        
        // ========== 4. 复制算法 ==========
        // 把内存分成两块,每次只使用一块
        // 清理时,把存活的对象复制到另一块,清理当前块
        // 缺点:可用内存减半
        
        // ========== 5. 标记-整理算法 ==========
        // 标记后,让存活对象向一端移动,然后清理边界外内存
        // 适合老年代
        
        // ========== 6. 分代收集算法 ==========
        // 新生代:复制算法(少量对象存活)
        // 老年代:标记-整理算法(大量对象存活)
    }
}

JVM内存分代模型:

┌─────────────────────────────────────────────────────────────────────┐
│                      JVM内存分代模型                                 │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                         JVM进程内存                          │   │
│   │                                                              │   │
│   │   ┌───────────────────────────────────────────────────────┐ │   │
│   │   │              方法区(Method Area)                      │ │   │
│   │   │   - 类信息、常量、静态变量                              │ │   │
│   │   │   - 运行时常量池                                       │ │   │
│   │   │   - JIT编译后的代码                                    │ │   │
│   │   └───────────────────────────────────────────────────────┘ │   │
│   │                                                              │   │
│   │   ┌───────────────────────────────────────────────────────┐ │   │
│   │   │                    堆(Heap)                          │ │   │
│   │   │                                                              │ │   │
│   │   │  ┌─────────────────────────────────────────────────┐  │ │   │
│   │   │  │              老年代(Old Generation)            │  │ │   │
│   │   │  │   容量: Eden + Survivor × 2                    │  │ │   │
│   │   │  │   垃圾回收:Major GC / Full GC                   │  │ │   │
│   │   │  │   算法:标记-整理 / 标记-清除                     │  │ │   │
│   │   │  └─────────────────────────────────────────────────┘  │ │   │
│   │   │                        ↑                               │ │   │
│   │   │                        │ 晋升                          │ │   │
│   │   │                        │                               │ │   │
│   │   │  ┌────────────┬────────┴────────┬───────────┐        │ │   │
│   │   │  │            │                 │           │        │ │   │
│   │   │  │   Eden     │  Survivor From  │ SurvivorTo│        │ │   │
│   │   │  │  (伊甸园)  │   (From)        │  (To)     │        │ │   │
│   │   │  │            │                 │           │        │ │   │
│   │   │  │  8/101/101/10    │        │ │   │
│   │   │  │            │                 │           │        │ │   │
│   │   │  └────────────┴─────────────────┴───────────┘        │ │   │
│   │   │                    新生代(Young Generation)          │ │   │
│   │   │   垃圾回收:Minor GC / Young GC                        │ │   │
│   │   │   算法:复制算法                                        │ │   │
│   │   └───────────────────────────────────────────────────────┘ │   │
│   │                                                              │   │
│   │   ┌───────────────────────────────────────────────────────┐ │   │
│   │   │              虚拟机栈(VM Stack)                      │ │   │
│   │   │   每个线程私有                                          │ │   │
│   │   │   栈帧:局部变量表、操作数栈、动态链接、方法返回       │ │   │
│   │   └───────────────────────────────────────────────────────┘ │   │
│   │                                                              │   │
│   │   ┌───────────────────────────────────────────────────────┐ │   │
│   │   │              本地方法栈(Native Method Stack)        │ │   │
│   │   │   Native方法使用                                       │ │   │
│   │   └───────────────────────────────────────────────────────┘ │   │
│   │                                                              │   │
│   │   ┌───────────────────────────────────────────────────────┐ │   │
│   │   │              程序计数器(Program Counter)             │ │   │
│   │   │   每个线程私有,记录当前线程执行的字节码行号           │ │   │
│   │   └───────────────────────────────────────────────────────┘ │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

对象分配与回收流程:

┌─────────────────────────────────────────────────────────────────────┐
│                      对象分配与回收流程                              │
│                                                                      │
│   1️⃣ 新对象优先在Eden区分配                                          │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Eden区                   Survivor区                        │   │
│   │  ┌──────────────────┐    ┌───────────┬───────────┐         │   │
│   │  │                  │    │    FromTo     │         │   │
│   │  │   [新对象]       │    │           │           │         │   │
│   │  │                  │    │           │           │         │   │
│   │  └──────────────────┘    └───────────┴───────────┘         │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   2️⃣ Minor GC(新生代垃圾回收)                                      │
│                                                                      │
│   Eden区满 → Minor GC → 存活对象复制到Survivor区                     │
│   经历N次Minor GC后,晋升到老年代(默认15岁)                         │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                        回收前                               │   │
│   │                                                              │   │
│   │  Eden区:  [存活] [存活] [垃圾] [存活] [垃圾] [垃圾]        │   │
│   │  Survivor From[存活] [存活]                                │   │
│   │  Survivor To:  (空)                                        │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│                              │                                       │
│                              ▼ GC                                   │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                        回收后                               │   │
│   │                                                              │   │
│   │  Eden区:  (清空)                                           │   │
│   │  Survivor From:(清空)                                      │   │
│   │  Survivor To[存活] [存活]                                │   │
│   │                                                              │   │
│   │  年龄+1                                                      │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   3️⃣ Full GC(老年代垃圾回收)                                       │
│                                                                      │
│   触发条件:                                                         │
│   - 老年代空间不足                                                   │
│   - System.gc()                                                      │
│   - MetaSpace空间不足                                                │
│   - Minor GC时,Survivor区放不下                                     │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

常见GC器组合:

┌─────────────────────────────────────────────────────────────────────┐
│                      常见GC器组合                                    │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                       Serial + Serial Old                     │   │
│   │   特点:单线程、最简单、Stop The World最长                     │   │
│   │   适用:客户端模式、单核CPU                                    │   │
│   │   -XX:+UseSerialGC                                            │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                   Parallel Scavenge + Serial Old              │   │
│   │   特点:并行、吞吐量优先                                       │   │
│   │   适用:后台运算,不需要太多交互                               │   │
│   │   -XX:+UseParallelGC                                          │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                  Parallel Scavenge + Parallel Old            │   │
│   │   特点:并行、吞吐量优先                                       │   │
│   │   适用:注重吞吐量的场景                                       │   │
│   │   -XX:+UseParallelOldGC                                       │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                      ParNew + CMS                             │   │
│   │   特点:并发、低延迟                                           │   │
│   │   适用:注重响应时间的场景                                     │   │
│   │   -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection         │   │
│   │                                                              │   │
│   │   CMS工作流程:                                               │   │
│   │   1. 初始标记(STW)— 标记GC Roots直接引用的对象              │   │
│   │   2. 并发标记 — 遍历引用链                                    │   │
│   │   3. 重新标记(STW)— 修正并发标记期间的变动                  │   │
│   │   4. 并发清除 — 清除垃圾                                      │   │
│   │                                                              │   │
│   │   缺点:                                                       │   │
│   │   - 无法处理浮动垃圾(并发时新产生的垃圾)                    │   │
│   │   - 内存碎片化(需要Full GC整理)                             │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                         G1                                   │   │
│   │   特点:分区、可控STW、可并发                                 │   │
│   │   适用:JDK 9+默认,推荐使用                                   │   │
│   │   -XX:+UseG1GC                                                │   │
│   │                                                              │   │
│   │   G1将堆划分为多个大小相等的Region:                          │   │
│   │   ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐          │   │
│   │   │ E   │ E   │ S   │ O   │ O   │ H   │ E   │ O   │          │   │
│   │   │(Eden)│     │(Sur)│(Old)│     │(大对象)│     │     │          │   │
│   │   └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘          │   │
│   │                                                              │   │
│   │   回收过程:                                                   │   │
│   │   1. Young GC — 年轻代Region收集                              │   │
│   │   2. Mixed GC — 年轻代 + 若干老年代Region收集                  │   │
│   │   3. Full GC — 当G1无法跟上分配速度时                        │   │
│   │                                                              │   │
│   │   设置目标延迟:-XX:MaxGCPauseMillis=200                      │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                      ZGC / Shenandoah                        │   │
│   │   特点:极低延迟(<10ms)、并发、不压缩堆                     │   │
│   │   适用:大内存、低延迟要求的场景                              │   │
│   │   -XX:+UseZGC                                                 │   │
│   │   -XX:+UseShenandoahGC                                        │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

四、MySQL数据库:索引与事务

4.1 InnoDB索引结构:B+树

为什么MySQL选择B+树作为索引结构?这是面试必问题!

二叉树 vs 红黑树 vs B树 vs B+树:

-- 创建索引
CREATE INDEX idx_user_name ON user(name);

-- 查看表结构
SHOW CREATE TABLE user\G
┌─────────────────────────────────────────────────────────────────────┐
│                      索引结构对比                                    │
│                                                                      │
│   1️⃣ 二叉树(Binary Tree)                                            │
│                                                                      │
│                              50                                      │
│                             /  \                                     │
│                           30    70                                  │
│                          /  \   /  \                                │
│                        20   40 60   80                               │
│                                                                      │
│   问题:如果数据递增插入,退化成链表!                               │
│   时间复杂度从O(logN)退化成O(N)                                      │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  5060708090 → ...                             │   │
│   │  /    /    /    /    /                                      │   │
│   │ 50   60   70   80   90  (变成链表)                          │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   2️⃣ 红黑树(自平衡二叉查找树)                                       │
│                                                                      │
│   特点:自动平衡,但高度仍然较高                                     │
│   问题:数据量大时,树高 = logN可能达到20+,                        │
│         每层都是一次IO操作,查询太慢!                              │
│                                                                      │
│   3️⃣ B树(B-Tree,多路平衡查找树)                                    │
│                                                                      │
│                         [50]                                        │
│                      /    |    \                                     │
│               [20,30]  [40]  [60,70,80]                             │
│                                                                      │
│   B树特点:                                                          │
│   - 每个节点可以有多个数据                                           │
│   - 节点包含数据,叶子节点不在同一层                                 │
│   - 所有节点都存储数据                                               │
│                                                                      │
│   问题:B树每个节点都存储数据,如果数据大,                          │
│         每个节点能容纳的key就少,树高增加                            │
│                                                                      │
│   4️⃣ B+树(B+Tree)— MySQL选择的结构                                   │
│                                                                      │
│                         [50]                                        │
│                      /    |    \                                     │
│               [20,30]  [40]  [60,70,80]                              │
│                  |      |       |                                   │
│   ┌──────────────┼──────┼───────┼──────────────────┐              │
│   │              │      │       │                  │              │
│   [所有数据]  [所有数据] [所有数据]  ...                  │              │
│                                                                      │
│   B+树特点:                                                          │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ✓ 非叶子节点只存储键(索引),不存储数据                    │   │
│   │  ✓ 叶子节点包含所有数据,按顺序链表连接                      │   │
│   │  ✓ 查询稳定,无论查哪个数据,都需要走到叶子节点              │   │
│   │  ✓ 树高更矮(通常3-4层),IO次数更少                         │   │
│   │  ✓ 范围查询友好(链表遍历)                                  │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

B+树在MySQL中的实现:

┌─────────────────────────────────────────────────────────────────────┐
                      InnoDB B+树索引结构                             
                                                                      
   InnoDB存储引擎以页为基本单位管理磁盘                               
   默认页大小 = 16KB                                                  
                                                                      
   ┌─────────────────────────────────────────────────────────────┐   
                       B+树结构                                     
                                                                    
                           [根节点]                                  
                        16KB / 14B  1170 个key                    
                                                                    
             ┌──────────┼──────────┐                                
                                                                  
        [索引页1]  [索引页2]  [索引页3]  ...                          
        1170个key   1170个key   1170个key                             
                                                                    
      ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐            
                                                          
                                                          
     数据   数据   数据   数据   数据   数据   数据   数据           
     页1    页2    页3    页4    页5    页6    页7    页8             
                                                                    
     每个数据页  16KB / 每行数据大小                                
     如果每行1KB,则每页约16条数据                                    
                                                                    
   └─────────────────────────────────────────────────────────────┘   
                                                                      
   计算一棵3层B+树能存多少数据:                                       
                                                                      
   ┌─────────────────────────────────────────────────────────────┐   
     根节点:1170个索引                   (1页)                     
                                                                    
     每个索引指向一个下一页                                          
     1170个索引  1170个叶子节点                                    
                                                                    
     每个叶子节点:16KB / 1KB  16条数据                            
     1170 × 16  18,720 条数据                                      
                                                                    
     结论:3层B+树可存储约2万条数据                                  
                                                                    
     实际场景(每行数据更小,如200B):                              
     16KB / 200B = 80条/页                                           
     1170 × 1170 × 80  1亿+ 条数据                                  
   └─────────────────────────────────────────────────────────────┘   
                                                                      
└─────────────────────────────────────────────────────────────────────┘

主键索引 vs 聚簇索引 vs 辅助索引:

-- 主键索引(聚簇索引)
-- 数据和索引在一起,叶子节点存储整行数据
ALTER TABLE user ADD PRIMARY KEY (id);

-- 辅助索引(非聚簇索引)
-- 叶子节点存储主键值
CREATE INDEX idx_name ON user(name);
┌─────────────────────────────────────────────────────────────────────┐
│                      聚簇索引 vs 辅助索引                           │
│                                                                      │
│   聚簇索引(Clustered Index):                                        │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                              │   │
│   │   主键 → 数据                                                │   │
│   │                                                              │   │
│   │   叶子节点存储:                                             │   │
│   │   ┌─────────────────────────────────────────────────────┐   │   │
│   │   │ id=1, name=Tom, age=25, email=Tom@xxx.com          │   │   │
│   │   │ id=2, name=Jack, age=30, email=Jack@xxx.com        │   │   │
│   │   │ id=3, name=Lucy, age=28, email=Lucy@xxx.com         │   │   │
│   │   └─────────────────────────────────────────────────────┘   │   │
│   │                                                              │   │
│   │   特点:                                                     │   │
│   │   ✓ 一个表只能有一个聚簇索引                                 │   │
│   │   ✓ 数据按主键顺序物理存储                                   │   │
│   │   ✓ 主键查询快(直接命中)                                   │   │
│   │   ✓ 范围查询快                                               │   │
│   │   ✓ 插入/删除可能导致页分裂                                  │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   辅助索引(Secondary Index):                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                              │   │
│   │   name → 主键id                                              │   │
│   │                                                              │   │
│   │   叶子节点存储:                                             │   │
│   │   ┌─────────────────────────────────────────────────────┐   │   │
│   │   │ Jack → id=2                                         │   │   │
│   │   │ Lucy → id=3                                        │   │   │
│   │   │ Tom → id=1                                         │   │   │
│   │   └─────────────────────────────────────────────────────┘   │   │
│   │                                                              │   │
│   │   特点:                                                     │   │
│   │   ✓ 一个表可以有多个辅助索引                                 │   │
│   │   ✓ 叶子节点存储主键值,不是完整数据                         │   │
│   │   ✓ 先查辅助索引获取主键,再查聚簇索引(回表)               │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   回表查询:                                                         │
│                                                                      │
│   SELECT * FROM user WHERE name = 'Tom';                            │
│                                                                      │
│   执行过程:                                                         │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                              │   │
│   │   1️⃣ 在idx_name(辅助索引)中查找'Tom'                      │   │
│   │      → 得到主键 id=1                                        │   │
│   │                                                              │   │
│   │   2️⃣ 拿着id=1回到主键索引(聚簇索引)查找                    │   │
│   │      → 得到完整行数据                                        │   │
│   │                                                              │   │
│   │   这就是"回表查询"                                           │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   优化:覆盖索引(using index)                                       │
│                                                                      │
│   SELECT id, name FROM user WHERE name = 'Tom';                     │
│   -- 只查id和name,而name是索引列                                   │
│   -- 只需要扫描辅助索引,无需回表                                    │
│   EXPLAIN: type=index, Extra=Using index                            │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

五、Redis缓存:分布式系统必备

5.1 Redis为什么这么快?

Redis的高性能是面试必问,但要答出深度!

Redis为什么快?

┌─────────────────────────────────────────────────────────────────────┐
│                      Redis高性能的秘密                              │
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                     六大原因                                  │   │
│   │                                                              │   │
│   │   1️⃣ 内存存储                                                 │   │
│   │      数据存在内存中,读写速度极快(纳秒级)                    │   │
│   │      RAM速度 >> SSD >> HDD                                    │   │
│   │                                                              │   │
│   │   2️⃣ 单线程模型                                               │   │
│   │      ┌─────────────────────────────────────────────────┐    │   │
│   │      │                                                 │    │   │
│   │      │           Redis Server                          │    │   │
│   │      │           ┌───────────┐                        │    │   │
│   │      │           │ 主线程    │                        │    │   │
│   │      │           │ 单线程    │                        │    │   │
│   │      │           └───────────┘                        │    │   │
│   │      │                                                 │    │   │
│   │      │   优点:无需考虑锁问题,代码简单                 │    │   │
│   │      │   缺点:CPU是瓶颈时,无法利用多核                 │    │   │
│   │      │                                                 │    │   │
│   │      └─────────────────────────────────────────────────┘    │   │
│   │                                                              │   │
│   │   3️⃣ IO多路复用                                             │   │
│   │      ┌─────────────────────────────────────────────────┐    │   │
│   │      │  select/poll/epoll/kqueue                        │    │   │
│   │      │  一个线程同时监听多个socket                        │    │   │
│   │      │  有数据就处理,没有就阻塞                          │    │   │
│   │      └─────────────────────────────────────────────────┘    │   │
│   │                                                              │   │
│   │   4️⃣ 高效数据结构                                           │   │
│   │      String / List / Hash / Set / ZSet / Bitmap / Geo...  │   │
│   │      每种数据结构针对不同场景优化                           │   │
│   │                                                              │   │
│   │   5️⃣ 纯C语言编写                                            │   │
│   │      接近操作系统,性能好                                   │   │
│   │                                                              │   │
│   │   6️⃣ 客户端与服务端在同一个机器上                            │   │
│   │      网络延迟小                                             │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

5.2 Redis分布式锁

分布式锁是面试高频题,也是实际开发中的难点!

Redis实现分布式锁:

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis分布式锁实现
 */
public class RedisLock {
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    // 锁前缀
    private static final String LOCK_PREFIX = "lock:";
    
    // 唯一标识(区分不同JVM实例)
    private final String instanceId;
    
    public RedisLock(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.instanceId = UUID.randomUUID().toString();
    }
    
    /**
     * 获取锁(SETNX + EXPIRE)
     * 
     * 注意:分开执行不是原子操作!
     * 更好的方式是 SET key value NX EX seconds
     */
    public boolean tryLock(String key, long expireTime) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = instanceId + ":" + Thread.currentThread().getId();
        
        // 方式1:分开执行(不安全)
        // Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
        // if (success) {
        //     redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
        // }
        
        // 方式2:原子操作(推荐)
        // SET key value NX EX seconds
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS)
        );
    }
    
    /**
     * 释放锁
     * 
     * 注意:不能直接删除!
     * 必须判断锁是否是自己的才能删除
     * 否则可能释放别人的锁
     */
    public boolean unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = instanceId + ":" + Thread.currentThread().getId();
        
        // Lua脚本保证原子性
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, 
            Collections.singletonList(lockKey), lockValue);
        
        return result != null && result > 0;
    }
    
    /**
     * 带重试的获取锁
     */
    public boolean tryLockWithRetry(String key, long expireTime, 
                                    int maxRetries, long retryInterval) {
        for (int i = 0; i < maxRetries; i++) {
            if (tryLock(key, expireTime)) {
                return true;
            }
            try {
                Thread.sleep(retryInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        return false;
    }
    
    /**
     * 看门狗机制(自动续期)
     * 
     * 问题:如果持有锁的线程执行时间过长,
     * 锁会在expireTime后自动释放,导致其他线程获取锁
     * 
     * 解决:启动一个后台线程,自动续期
     */
    private static final long WATCHDOG_EXPIRE_TIME = 30;  // 30秒
    private static final long WATCHDOG_RENEW_INTERVAL = 10;  // 10秒续期一次
    
    public class WatchdogTask implements Runnable {
        private final String lockKey;
        private final String lockValue;
        private volatile boolean running = true;
        
        public WatchdogTask(String lockKey, String lockValue) {
            this.lockKey = lockKey;
            this.lockValue = lockValue;
        }
        
        public void stop() {
            running = false;
        }
        
        @Override
        public void run() {
            while (running) {
                try {
                    Thread.sleep(WATCHDOG_RENEW_INTERVAL * 1000);
                    
                    // 续期
                    String luaScript = 
                        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "    return redis.call('expire', KEYS[1], ARGV[2]) " +
                        "else " +
                        "    return 0 " +
                        "end";
                    
                    redisTemplate.execute(
                        new DefaultRedisScript<>(luaScript, Long.class),
                        Collections.singletonList(lockKey),
                        lockValue,
                        WATCHDOG_EXPIRE_TIME
                    );
                    
                } catch (Exception e) {
                    // 续期失败
                    break;
                }
            }
        }
    }
}

Redis分布式锁注意事项:

┌─────────────────────────────────────────────────────────────────────┐
│                      Redis分布式锁注意事项                          │
│                                                                      │
│   ⚠️ 问题1:锁续期                                                  │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  场景:线程A获取锁,30秒过期,但业务需要35秒                  │   │
│   │  结果:30秒后锁自动释放,线程B获取锁                          │   │
│   │       线程A执行完,释放线程B的锁                              │   │
│   │                                                              │   │
│   │  解决:看门狗机制(Watchdog),自动续期                       │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ⚠️ 问题2:主从切换                                                 │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  场景:Redis主从架构                                          │   │
│   │       线程A在master获取锁                                     │   │
│   │       master宕机,slave还没同步                              │   │
│   │       slave升级为master                                      │   │
│   │       线程B在新master获取锁(同一key!)                     │   │
│   │       两个线程同时持有锁!                                    │   │
│   │                                                              │   │
│   │  解决:RedLock算法(多master多数投票)                       │   │
│   │       或使用Redisson客户端                                   │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ⚠️ 问题3:可重入性                                                 │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  场景:同一个线程多次获取同一把锁                            │   │
│   │                                                              │   │
│   │  解决:计数器记录重入次数                                    │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   ✅ 推荐方案:                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                              │   │
│   │   1. Redisson(推荐)                                        │   │
│   │      - 支持可重入锁、读写锁、公平锁                          │   │
│   │      - 内置看门狗,自动续期                                  │   │
│   │      - 支持RedLock                                          │   │
│   │                                                              │   │
│   │   2. 开源框架                                                │   │
│   │      - Redisson                                             │   │
│   │      - ShardingJedis                                        │   │
│   │                                                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

六、Spring框架:核心原理

6.1 Spring循环依赖解决

循环依赖是Spring最经典的面试题之一!

三级缓存解决循环依赖:

/**
 * Spring三级缓存源码解析
 */
public class CircularDependencyDemo {
    
    public static void main(String[] args) {
        // Spring的三级缓存
        // DefaultSingletonBeanRegistry类
        
        // 一级缓存:已完成的单例Bean
        // private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
        
        // 二级缓存:提前暴露的Bean(未完成属性填充)
        // private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
        
        // 三级缓存:Bean工厂
        // private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
    }
}

/**
 * getSingleton源码
 */
public class DefaultSingletonBeanRegistry {
    
    public Object getSingleton(String beanName) {
        return getSingleton(beanName, true);
    }
    
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 1️⃣ 先从一级缓存获取
        Object singletonObject = singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // 2️⃣ 一级缓存没有,从二级缓存获取
            synchronized (this.singletonObjects) {
                singletonObject = earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    // 3️⃣ 二级缓存也没有,从三级缓存获取工厂
                    ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        // 调用工厂获取早期引用
                        singletonObject = singletonFactory.getObject();
                        // 放入二级缓存
                        earlySingletonObjects.put(beanName, singletonObject);
                        // 从三级缓存移除
                        singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }
    
    /**
     * 添加单例工厂到三级缓存
     */
    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        synchronized (this.singletonObjects) {
            if (!this.singletonObjects.containsKey(beanName)) {
                this.singletonFactories.put(beanName, singletonFactory);
                this.earlySingletonObjects.remove(beanName);
            }
        }
    }
}

循环依赖解决流程图:

┌─────────────────────────────────────────────────────────────────────┐
                  Spring三级缓存解决循环依赖                          
                                                                      
   场景:ServiceA依赖ServiceB,ServiceB依赖ServiceA                    
                                                                      
   ┌─────────────────────────────────────────────────────────────┐   
     三级缓存结构                                                   
                                                                    
     一级缓存 singletonObjects                                     
     ├── 完全初始化好的Bean                                         
     └── 可以直接使用                                               
                                                                    
     二级缓存 earlySingletonObjects                                
     ├── 提前曝光的Bean                                             
     └── 未完全初始化(属性填充初始化方法未执行)                  
                                                                    
     三级缓存 singletonFactories                                    
     ├── ObjectFactory工厂                                          
     └── 用于创建早期Bean的引用                                     
                                                                    
   └─────────────────────────────────────────────────────────────┘   
                                                                      
   解决流程:                                                         
                                                                      
   步骤1:创建ServiceA                                                
   ┌─────────────────────────────────────────────────────────────┐   
                                                                   
     1️⃣ 调用构造方法,创建原始对象A                                 
     2️⃣ 放入三级缓存singletonFactories                             
        { "serviceA": ObjectFactory<ServiceA> }                    
                                                                   
   └─────────────────────────────────────────────────────────────┘   
                                                                     
                                                                     
   步骤2:填充ServiceA的属性,发现依赖ServiceB                         
   ┌─────────────────────────────────────────────────────────────┐   
                                                                   
     @Autowired                                                    
     private ServiceB serviceB;                                    
                                                                   
      尝试获取ServiceB                                            
      ServiceB还没创建!需要创建ServiceB                           
                                                                   
   └─────────────────────────────────────────────────────────────┘   
                                                                     
                                                                     
   步骤3:创建ServiceB                                                
   ┌─────────────────────────────────────────────────────────────┐   
                                                                   
     1️⃣ 调用构造方法,创建原始对象B                                 
     2️⃣ 放入三级缓存singletonFactories                             
        { "serviceB": ObjectFactory<ServiceB> }                    
                                                                   
   └─────────────────────────────────────────────────────────────┘   
                                                                     
                                                                     
   步骤4:填充ServiceB的属性,发现依赖ServiceA                          
   ┌─────────────────────────────────────────────────────────────┐   
                                                                   
     @Autowired                                                    
     private ServiceA serviceA;                                    
                                                                   
      尝试获取ServiceA                                            
      从三级缓存获取ServiceA的ObjectFactory                      
      调用getObject()获取早期引用                                 
      放入二级缓存earlySingletonObjects                          
      从三级缓存移除                                              
                                                                   
     此时:                                                         
     - 一级缓存:{}                                                
     - 二级缓存:{"serviceA": 早期A}                               
     - 三级缓存:{"serviceB": ObjectFactory}                      
                                                                   
   └─────────────────────────────────────────────────────────────┘   
                                                                     
                                                                     
   步骤5ServiceB填充完成                                            
   ┌─────────────────────────────────────────────────────────────┐   
                                                                   
     1️⃣ 放入一级缓存singletonObjects                               
     2️⃣ 从二级缓存移除                                             
     3️⃣ 从三级缓存移除                                             
                                                                   
     此时:                                                         
     - 一级缓存:{"serviceB": 完全B}                               
     - 二级缓存:{"serviceA": 早期A}                               
     - 三级缓存:{}                                                
                                                                   
   └─────────────────────────────────────────────────────────────┘   
                                                                     
                                                                     
   步骤6:回到ServiceA,填充ServiceB                                  
   ┌─────────────────────────────────────────────────────────────┐   
                                                                   
     从一级缓存获取ServiceB                                         
      ServiceA填充完成                                            
      放入一级缓存singletonObjects                               
                                                                   
     完成!循环依赖解决!                                           
                                                                   
   └─────────────────────────────────────────────────────────────┘   
                                                                      
└─────────────────────────────────────────────────────────────────────┘

七、面试高频问题总结

7.1 技术问题清单

┌─────────────────────────────────────────────────────────────────────┐
│                      面试高频问题                            │
│                                                                      │
│   【Java基础】                                                        │
│   ├── String为什么不可变?                                          │
│   ├── String、StringBuilder、StringBuffer区别?                     │
│   ├── HashMap和Hashtable区别?                                       │
│   ├── HashMap底层实现(JDK 7 vs JDK 8)?                           │
│   ├── ConcurrentHashMap如何保证线程安全?                           │
│   ├── Object的hashCode和equals方法?                                 │
│   └── final关键字作用?                                              │
│                                                                      │
│   【并发编程】                                                        │
│   ├── synchronized和ReentrantLock区别?                             │
│   ├── volatile关键字作用?                                           │
│   ├── ThreadLocal原理?内存泄漏问题?                                │
│   ├── 线程池有哪些?拒绝策略?                                       │
│   ├── CAS原理?ABA问题?                                             │
│   ├── AQS原理?                                                      │
│   └── 生产者消费者模式实现?                                         │
│                                                                      │
│   【JVM】                                                             │
│   ├── JVM内存区域划分?                                              │
│   ├── 什么对象会被回收?GC Roots有哪些?                             │
│   ├── 垃圾回收算法?                                                │
│   ├── 常见垃圾收集器?G1和CMS区别?                                  │
│   ├── 类加载机制?双亲委派模型?                                     │
│   ├── JVM调优参数?                                                  │
│   └── OOM如何排查?                                                  │
│                                                                      │
│   【MySQL】                                                           │
│   ├── InnoDB和MyISAM区别?                                           │
│   ├── 索引数据结构?为什么是B+树?                                   │
│   ├── 主键索引和辅助索引区别?                                       │
│   ├── 聚簇索引和非聚簇索引?                                         │
│   ├── 事务隔离级别?MVCC原理?                                       │
│   ├── MySQL如何实现可重复读?                                        │
│   ├── 慢查询如何优化?                                              │
│   └── 分库分表策略?                                                │
│                                                                      │
│   【Redis】                                                           │
│   ├── Redis为什么快?                                                │
│   ├── Redis数据类型?应用场景?                                      │
│   ├── Redis持久化?RDB和AOF区别?                                   │
│   ├── Redis过期策略?内存淘汰策略?                                  │
│   ├── Redis分布式锁?主从切换问题?                                 │
│   ├── Redis和Memcached区别?                                        │
│   └── Redis缓存问题?穿透、击穿、雪崩?                             │
│                                                                      │
│   【Spring】                                                          │
│   ├── Spring IOC流程?                                               │
│   ├── Bean生命周期?                                                 │
│   ├── 循环依赖如何解决?三级缓存?                                   │
│   ├── AOP原理?JDK和CGLIB区别?                                      │
│   ├── @Transactional失效场景?                                       │
│   └── Spring事务传播行为?                                           │
│                                                                      │
│   【分布式/微服务】                                                   │
│   ├── CAP定理?                                                      │
│   ├── BASE理论?                                                     │
│   ├── 一致性Hash算法?                                               │
│   ├── 分布式Session共享?                                           │
│   ├── 幂等性如何保证?                                               │
│   └── 分布式ID生成方案?                                             │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

7.2 面试技巧

┌─────────────────────────────────────────────────────────────────────┐
│                      面试技巧大公开                                  │
│                                                                      │
│   1️⃣ STAR法则回答问题                                                │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  S - Situation(情境): 项目背景是什么                      │   │
│   │  T - Task(任务): 你负责什么                               │   │
│   │  A - Action(行动): 你做了什么                             │   │
│   │  R - Result(结果): 结果如何,学到什么                     │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   2️⃣ 技术问题回答结构                                                │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  是什么 → 解决了什么问题 → 底层原理 → 实际应用              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   3️⃣ 不会的问题处理                                                  │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ✓ 诚实承认不了解                                          │   │
│   │  ✓ 但展示你的思路和好奇心                                  │   │
│   │  ✓ 尝试联系到已知知识                                      │   │
│   │  ✗ 不要瞎编乱造                                            │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   4️⃣ 反问环节必问                                                   │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ✓ 技术栈是什么                                            │   │
│   │  ✓ 团队规模和分工                                         │   │
│   │  ✓ 技术挑战和成长机会                                     │   │
│   │  ✓ 业务和技术比重                                         │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   5️⃣ 薪资谈判技巧                                                   │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ✓ 先了解市场行情                                          │   │
│   │  ✓ 给出期望区间,而非具体数字                               │   │
│   │  ✓ 强调你的价值和不可替代性                                │   │
│   │  ✓ 不要只谈钱,也谈成长和平台                              │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

结语

求职旺季,是每一位开发者展示自己的舞台。希望这篇文章能帮助你在面试中:

✅ 不仅能答出"是什么",更能答出"为什么"

✅ 不仅能说出结论,更能讲清原理

✅ 不仅能应对面试官,更能展现你的技术深度

记住:好的面试不是背答案,而是真正理解技术的本质。

最后,祝每一位小伙伴都能拿到心仪的Offer!💰


🎯 讨论话题: 大家在面试中还遇到过哪些"灵魂拷问"?欢迎在评论区分享!

👇 点击下方投票,告诉我你最想深入了解哪个专题:

  • 并发编程(JUC)
  • JVM调优
  • MySQL优化
  • Redis高级特性
  • 分布式系统设计

本文首发于掘金,同步更新于CSDN

如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 👀

你的支持是我创作的最大动力!