涵盖:底层原理 · 第三方库 · 实战难题 · 性能优化 · 横向对比 · 高难度深层问题 全面、融会贯通、深入浅出、细节到位
目录
- 第一章 JVM 深度剖析
- 第二章 Java 语言核心底层
- 第三章 并发编程深度
- 第四章 集合框架底层
- 第五章 IO/NIO/AIO 深度
- 第六章 Spring 全家桶底层原理
- 第七章 MyBatis 底层原理
- 第八章 中间件原理与八股文
- 第九章 数据库与 ORM 深度
- 第十章 分布式系统原理
- 第十一章 实战难题与解决方案
- 第十二章 性能优化深度指南
- 第十三章 全面横向对比
- 第十四章 高难度深底层原理问题
第一章 JVM 深度剖析
1.1 JVM 内存模型(运行时数据区)
1.1.1 程序计数器(Program Counter Register)
- 线程私有,生命周期与线程一致
- 记录当前线程执行的字节码行号(native 方法时为 undefined)
- 唯一不会发生 OOM 的内存区域
- 字节码解释器通过改变计数器值来选取下一条指令,分支、循环、异常处理、线程恢复都依赖它
- 多线程切换时,每个线程需要独立的计数器来恢复正确的执行位置
1.1.2 虚拟机栈(VM Stack)
- 线程私有,描述 Java 方法执行的内存模型
- 每个方法执行时创建栈帧(Stack Frame)
- 栈帧内部结构:
- 局部变量表:存放编译期可知的基本数据类型、对象引用(reference)、returnAddress 类型。64 位的 long/double 占 2 个 Slot,其余占 1 个 Slot。局部变量表大小在编译时确定,运行时不变
- 操作数栈:后进先出(LIFO),最大深度编译期确定。算术运算、方法调用参数传递都通过操作数栈
- 动态链接:栈帧中包含指向运行时常量池中该帧所属方法的引用,支持方法调用过程中的动态连接
- 方法返回地址:正常退出时 PC 计数器值作为返回地址;异常退出通过异常处理表确定
- StackOverflowError:线程请求的栈深度超过虚拟机允许的最大深度
- OutOfMemoryError:虚拟机栈动态扩展时无法申请到足够内存
1.1.3 本地方法栈(Native Method Stack)
- 为 native 方法服务
- HotSpot 中虚拟机栈和本地方法栈合二为一
- 同样可能抛出 StackOverflowError 和 OutOfMemoryError
1.1.4 堆(Heap)
- 线程共享,虚拟机启动时创建
- 唯一目的:存放对象实例(几乎所有)
- GC 的主要管理区域,又称"GC 堆"
- 分代设计(非强制,G1 打破了物理分代):
- 新生代(Young Generation):Eden + Survivor0 + Survivor1,默认比例 8:1:1
- 老年代(Old/Tenured Generation)
- JDK 8 移除永久代,引入 Metaspace
- TLAB(Thread Local Allocation Buffer):每个线程在 Eden 区预先分配的小块内存,避免分配对象时的锁竞争。通过
-XX:+UseTLAB开启(默认开启) - 逃逸分析优化:栈上分配、标量替换、同步消除
1.1.5 方法区(Method Area)/ 元空间(Metaspace)
- 线程共享,存储类信息、常量、静态变量、JIT 编译后的代码
- JDK 7:永久代(PermGen),字符串常量池移到堆中
- JDK 8:移除永久代,引入 Metaspace(使用本地内存),类元数据存在本地内存
- Metaspace 参数:
-XX:MetaspaceSize(初始)、-XX:MaxMetaspaceSize(最大) - 为什么移除永久代?
- 永久代大小固定,容易 OOM
- 字符串常量池在永久代中导致性能问题
- 与 JRockit 合并的需要
1.1.6 直接内存(Direct Memory)
- 不属于 JVM 运行时数据区,但被频繁使用
- NIO 中 DirectByteBuffer 通过
Unsafe.allocateMemory()分配 - 受
-XX:MaxDirectMemorySize限制 - 避免了 Java 堆和 Native 堆之间的数据复制
1.1.7 运行时常量池
- 方法区的一部分
- Class 文件中的常量池表(字面量+符号引用)加载后存放于此
- 运行期间也可以将新常量放入:
String.intern() - JDK 7+ 字符串常量池移到堆中
1.2 对象创建的完整流程
- 类加载检查:检查 new 指令的参数能否在常量池中定位到类的符号引用,且该类是否已被加载、解析、初始化
- 分配内存:
- 指针碰撞(Bump the Pointer):堆内存规整时(Serial、ParNew 等带压缩的收集器),一个指针作为分界线
- 空闲列表(Free List):堆内存不规整时(CMS 等基于标记-清除的收集器),维护可用内存块列表
- 并发安全:CAS + 失败重试 或 TLAB
- 内存初始化零值:保证实例字段不赋初值就能使用
- 设置对象头:Mark Word + 类型指针(+ 数组长度)
- 执行
<init>方法:按代码中的赋值和构造函数初始化
1.3 对象内存布局
对象头(Object Header)
- Mark Word(32/64 位):
- 无锁状态:对象 hashCode(25bit) + 分代年龄(4bit) + 偏向锁标志(1bit) + 锁标志位(2bit)
- 偏向锁:线程 ID(23bit) + Epoch(2bit) + 分代年龄(4bit) + 偏向标志(1bit) + 锁标志(2bit)
- 轻量级锁:指向栈中锁记录的指针(30bit) + 锁标志(2bit)
- 重量级锁:指向 Monitor 的指针(30bit) + 锁标志(2bit)
- GC 标记:空(30bit) + 锁标志(2bit) = 11
- 类型指针(Klass Pointer):指向方法区中类元数据的指针。开启压缩指针(
-XX:+UseCompressedOops)时 4 字节,否则 8 字节 - 数组长度(如果是数组)
实例数据(Instance Data)
- 按分配策略排列:longs/doubles → ints → shorts/chars → bytes/booleans → oops(对象引用)
- 父类字段在子类字段之前
CompactFields允许子类较窄变量插入父类变量的间隙
对齐填充(Padding)
- HotSpot 要求对象起始地址必须是 8 字节的整数倍
- 不满 8 字节倍数时自动填充
1.4 垃圾回收深度
1.4.1 判断对象是否可回收
引用计数法:
- 给对象添加引用计数器,引用+1,引用失效-1,为0时回收
- 致命缺陷:循环引用无法解决(A→B,B→A,两者都不可达但计数都不为0)
- Python 使用引用计数 + 循环检测器解决
可达性分析(Reachability Analysis):
- 从 GC Roots 出发,通过引用链(Reference Chain)判断
- GC Roots 包括:
- 虚拟机栈中引用的对象(局部变量表)
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- JVM 内部引用(基本类型对应的 Class 对象、常驻异常对象、系统类加载器)
- 所有被 synchronized 持有的对象
- JMXBean、JVMTI 中注册的回调、本地代码缓存等
四种引用类型:
| 引用类型 | 回收时机 | 用途 | 实现类 |
|---|---|---|---|
| 强引用 | 永不回收(只要引用在) | 普通赋值 | Object obj = new Object() |
| 软引用 | 内存不足时回收 | 缓存 | SoftReference |
| 弱引用 | 下次 GC 时回收 | 缓存、防止内存泄漏 | WeakReference |
| 虚引用 | 随时可被回收 | 跟踪对象被回收的活动 | PhantomReference |
finalize() 机制:
- 对象不可达后并非直接回收,先进入"缓刑"
- 若覆写 finalize() 且未被调用过,放入 F-Queue 队列
- Finalizer 线程执行 finalize(),GC 再次标记时若仍不可达才真正回收
- finalize() 只会被调用一次,不推荐使用
1.4.2 垃圾回收算法
标记-清除(Mark-Sweep):
- 先标记所有需要回收的对象,然后统一回收
- 缺点:效率不稳定;产生大量内存碎片
标记-复制(Mark-Copy):
- 内存分为两半,只用一半,GC 时将存活对象复制到另一半
- 新生代的改进版:Eden:S0:S1 = 8:1:1,每次 GC 只复制存活对象到空的 Survivor
- 缺点:内存利用率只有50%(改进后 90%)
标记-整理(Mark-Compact):
- 标记后将存活对象向内存一端移动,清理边界外内存
- 适合老年代(存活率高)
- 缺点:移动对象开销大,需要 STW
分代收集理论:
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次 GC 的对象越难以消亡
- 跨代引用假说:跨代引用相对于同代引用仅占极少数 → 记忆集(Remembered Set)
1.4.3 垃圾收集器详解
Serial / Serial Old
- 单线程,STW
- 新生代用复制算法,老年代用标记-整理
- 简单高效,适合 Client 模式
ParNew
- Serial 的多线程版本
- 新生代并行收集
- 能与 CMS 配合工作
Parallel Scavenge / Parallel Old
- 关注吞吐量(= 用户代码时间 / (用户代码时间 + GC 时间))
-XX:MaxGCPauseMillis:最大 GC 停顿时间-XX:GCTimeRatio:吞吐量比值-XX:+UseAdaptiveSizePolicy:自适应调节策略
CMS(Concurrent Mark Sweep)
- 目标:最短回收停顿时间
- 算法:标记-清除
- 四个阶段:
- 初始标记(STW):标记 GC Roots 直接关联的对象,速度快
- 并发标记:从 GC Roots 开始遍历对象图,与用户线程并发
- 重新标记(STW):修正并发标记期间变动的引用(增量更新 Incremental Update)
- 并发清除:清理已标记的对象,与用户线程并发
- 缺点:
- CPU 资源敏感(并发阶段占用线程)
- 无法处理"浮动垃圾"(并发清除时新产生的垃圾)
- 标记-清除产生碎片 →
-XX:+UseCMSCompactAtFullGC - Concurrent Mode Failure → 退化为 Serial Old
G1(Garbage First)
- Region 化堆内存:将堆划分为多个大小相等的 Region(1-32MB)
- 每个 Region 可以是 Eden/Survivor/Old/Humongous
- Humongous Region:大于 Region 50% 的对象直接分配在 Humongous 区
- Mixed GC:可以选择性回收部分老年代 Region
- 四个阶段:
- 初始标记(STW):标记 GC Roots 直接关联对象,修改 TAMS 指针
- 并发标记:可达性分析,使用 SATB(Snapshot-At-The-Beginning)
- 最终标记(STW):处理 SATB 缓冲区中的引用变化
- 筛选回收(STW):按回收价值排序 Region,复制到空 Region
- 核心数据结构:
- Remembered Set(RSet):每个 Region 维护,记录其他 Region 指向本 Region 的引用
- Card Table:RSet 的实现方式,将内存划分为 512 字节的 Card
- Collection Set(CSet):本次 GC 要回收的 Region 集合
- SATB:并发标记开始时的对象图快照,通过写前屏障记录
ZGC(JDK 11+)
- 目标:亚毫秒级停顿(< 1ms),TB 级堆支持
- 核心技术:
- 着色指针(Colored Pointers):在指针中嵌入标记信息(Marked0、Marked1、Remapped、Finalizable),利用虚拟内存映射
- 读屏障(Load Barrier):在读取引用时插入屏障代码,判断指针颜色并修正
- 多重映射:同一块物理内存映射到三个虚拟地址(对应三种视图)
- 阶段:初始标记(STW) → 并发标记 → 再标记(STW) → 并发转移准备 → 初始转移(STW) → 并发转移
- Region 分类:Small(2MB)、Medium(32MB)、Large(N×2MB)
- 不分代(JDK 21 引入分代 ZGC)
Shenandoah(JDK 12+)
- 与 ZGC 类似的低延迟目标
- Brooks Pointer:每个对象头前加一个转发指针
- 并发转移时通过转发指针实现
- 非 Oracle 官方(RedHat 贡献)
1.4.4 三色标记法
- 白色:未被访问,分析结束后仍为白色的是不可达对象
- 灰色:已被访问,但至少有一个引用未被扫描
- 黑色:已被访问,所有引用都已扫描
并发标记的问题:
- 漏标(Missing Mark):满足两个条件同时成立:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
- 解决方案:
- 增量更新(Incremental Update):破坏条件1,记录新增的黑→白引用,重新标记时再扫描。CMS 使用
- SATB(Snapshot At The Beginning):破坏条件2,记录被删除的引用,维持标记开始时的对象图快照。G1 使用
1.4.5 安全点与安全区域
安全点(Safepoint):
- GC 时所有线程必须跑到安全点才能暂停
- 安全点位置:方法调用、循环跳转、异常跳转等"长时间执行"的指令
- 主动式中断:设置中断标志,线程轮询到标志时主动挂起(HotSpot 采用)
- 抢先式中断:直接中断所有线程,没到安全点的恢复运行(几乎不用)
安全区域(Safe Region):
- 解决线程处于 Sleep/Blocked 状态无法走到安全点的问题
- 线程进入安全区域时标识自己,离开时检查是否完成了根节点枚举
1.4.6 记忆集与卡表
- 记忆集(Remembered Set):抽象数据结构,记录从非收集区域指向收集区域的指针集合
- 卡表(Card Table):记忆集的一种实现,用字节数组实现,每个元素对应内存中一块区域(Card Page,通常 512 字节)
- 写屏障(Write Barrier):在引用赋值操作前后插入的代码,用于更新卡表
- 写前屏障(Pre-Write Barrier):SATB 使用
- 写后屏障(Post-Write Barrier):更新卡表
- 伪共享问题:多个 Card 在同一缓存行,
-XX:+UseCondCardMark先检查再标脏
1.5 类加载机制
1.5.1 类的生命周期
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
其中验证、准备、解析统称为连接(Linking)
1.5.2 各阶段详解
加载(Loading):
- 通过全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆中生成 java.lang.Class 对象,作为方法区数据的访问入口
验证(Verification):
- 文件格式验证:魔数 0xCAFEBABE、版本号等
- 元数据验证:是否有父类、是否继承了 final 类等
- 字节码验证:数据流和控制流分析
- 符号引用验证:确保解析能正确执行
准备(Preparation):
- 为类变量(static)分配内存并设置零值
- final static 在此阶段直接赋值(编译时常量)
- 实例变量不在此阶段分配
解析(Resolution):
- 将常量池中的符号引用替换为直接引用
- 类或接口解析、字段解析、方法解析、接口方法解析
初始化(Initialization):
- 执行类构造器
<clinit>()方法 <clinit>()由编译器自动收集类中所有类变量赋值语句和 static 块合并产生- 虚拟机保证
<clinit>()线程安全(隐式加锁) - 必须触发初始化的场景(主动引用):
- new/getstatic/putstatic/invokestatic 指令
- 反射调用
- 初始化类时父类未初始化先初始化父类
- main 方法所在类
- JDK 7 MethodHandle 解析结果为 REF_getStatic 等
1.5.3 类加载器
三层类加载器:
- Bootstrap ClassLoader:加载
<JAVA_HOME>/lib下核心类库,C++ 实现,Java 中表示为 null - Extension ClassLoader(JDK 9 后 Platform ClassLoader):加载
<JAVA_HOME>/lib/ext - Application ClassLoader:加载用户类路径(ClassPath)上的类
双亲委派模型(Parent Delegation Model):
- 收到类加载请求时,先委派父加载器加载
- 父加载器无法加载时,自己才尝试加载
- 作用:保证核心类库的安全性和唯一性(如 java.lang.Object 只会被 Bootstrap 加载)
打破双亲委派:
- SPI 机制:核心类需要加载第三方实现(JDBC、JNDI),通过 Thread.currentThread().getContextClassLoader() 加载
- OSGi:每个 Bundle 有自己的类加载器,实现模块化热部署
- Tomcat:每个 WebApp 有独立的 WebAppClassLoader,实现应用隔离
- JDK 9 模块系统:模块化改变了类加载的委派规则
1.5.4 Tomcat 类加载器架构
Bootstrap
↑
Extension
↑
Application
↑
Common ClassLoader ← 加载 Tomcat 公共类
↗ ↖
Catalina CL Shared CL ← 所有 WebApp 共享
↑
WebApp ClassLoader ← 每个应用独立
↑
Jsp ClassLoader ← 每个 JSP 独立(支持热替换)
- WebApp ClassLoader 先自己加载,加载不了再委派父加载器(打破双亲委派)
- 保证不同应用中相同全限定名的类互不干扰
1.6 JIT 编译器
1.6.1 解释执行 vs 编译执行
- 解释执行:逐行翻译字节码为机器码执行,启动快,运行慢
- 编译执行:将热点代码编译为本地机器码,启动慢,运行快
- HotSpot 采用混合模式:先解释执行,热点代码触发 JIT 编译
1.6.2 热点探测
- 基于计数器:
- 方法调用计数器:统计方法被调用次数,阈值
-XX:CompileThreshold(Client 1500,Server 10000) - 回边计数器:统计循环体执行次数(触发 OSR 栈上替换编译)
- 方法调用计数器:统计方法被调用次数,阈值
- 半衰周期:计数器值随时间衰减,
-XX:-UseCounterDecay关闭
1.6.3 C1 和 C2 编译器
- C1(Client Compiler):简单优化,编译速度快。方法内联、去虚拟化、冗余消除
- C2(Server Compiler):深度优化,编译质量高。逃逸分析、标量替换、循环展开、向量化
- 分层编译(Tiered Compilation):JDK 8 默认开启
- Level 0:解释执行
- Level 1:C1 编译,无 profiling
- Level 2:C1 编译,有限 profiling
- Level 3:C1 编译,完全 profiling
- Level 4:C2 编译
1.6.4 逃逸分析(Escape Analysis)
- 方法逃逸:对象被方法外部引用(作为返回值、赋给外部变量)
- 线程逃逸:对象被其他线程访问
- 不逃逸时的优化:
- 栈上分配:对象直接在栈帧上分配,方法结束自动销毁
- 标量替换:将对象拆散为基本类型变量
- 同步消除:消除不必要的锁
1.6.5 Graal 编译器
- JDK 10 引入,Java 编写的 JIT 编译器
- 可作为 C2 的替代品
- GraalVM 的核心组件
- 支持多语言(Polyglot)
1.7 JVM 调优核心参数
| 参数 | 含义 |
|---|---|
-Xms | 堆初始大小 |
-Xmx | 堆最大大小 |
-Xmn | 新生代大小 |
-Xss | 每个线程栈大小 |
-XX:MetaspaceSize | 元空间初始大小 |
-XX:MaxMetaspaceSize | 元空间最大大小 |
-XX:SurvivorRatio | Eden:Survivor 比值 |
-XX:MaxTenuringThreshold | 对象晋升老年代的年龄阈值 |
-XX:PretenureSizeThreshold | 直接进入老年代的对象大小阈值 |
-XX:+UseCompressedOops | 开启压缩指针 |
-XX:+PrintGCDetails | 打印 GC 详细日志 |
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump |
第二章 Java 语言核心底层
2.1 基本数据类型与包装类
2.1.1 八大基本类型
| 类型 | 大小(byte) | 范围 | 默认值 | 包装类 |
|---|---|---|---|---|
| byte | 1 | -128~127 | 0 | Byte |
| short | 2 | -32768~32767 | 0 | Short |
| int | 4 | -2^31~2^31-1 | 0 | Integer |
| long | 8 | -2^63~2^63-1 | 0L | Long |
| float | 4 | IEEE 754 | 0.0f | Float |
| double | 8 | IEEE 754 | 0.0d | Double |
| char | 2 | 0~65535 | '\u0000' | Character |
| boolean | ~1 | true/false | false | Boolean |
2.1.2 自动装箱/拆箱(Autoboxing/Unboxing)
- 编译器糖衣:
Integer.valueOf()和Integer.intValue() - 缓存池(Cache Pool):
- Integer:-128 ~ 127(可通过
-XX:AutoBoxCacheMax调整上界) - Byte、Short:-128 ~ 127
- Character:0 ~ 127
- Long:-128 ~ 127
- Boolean:TRUE / FALSE
- Float/Double:无缓存
- Integer:-128 ~ 127(可通过
经典陷阱:
Integer a = 127; Integer b = 127; a == b→ true(缓存)Integer a = 128; Integer b = 128; a == b→ false(新对象)- 拆箱时 NPE:
Integer a = null; int b = a;→ NullPointerException
2.2 String 底层原理
2.2.1 不可变性
- JDK 8:
private final char[] value; - JDK 9+:
private final byte[] value;+private final byte coder;(Compact Strings,Latin1 用 1 字节,其他用 2 字节) - 为什么不可变?
- 字符串常量池的前提
- hashCode 缓存(只计算一次)
- 线程安全
- 安全性(类加载、网络连接等使用字符串参数)
2.2.2 字符串常量池
- JDK 7+ 从永久代移到堆中
String.intern():如果池中已有相同内容的字符串,返回池中引用;否则(JDK 7+)将堆中字符串的引用放入池中- 字面量自动入池
经典问题:
String s1 = new String("abc"); // 创建了几个对象?
// 答:1个或2个。如果常量池中没有"abc",先在常量池创建一个,再在堆中创建一个。
// 如果常量池中已有"abc",只在堆中创建一个。
String s2 = new String("a") + new String("b");
// 实际上创建了:常量池"a"、常量池"b"、堆中new String("a")、堆中new String("b")、
// StringBuilder、StringBuilder.toString()产生的new String("ab")
// 注意:"ab"不会自动放入常量池
2.2.3 String vs StringBuilder vs StringBuffer
| 维度 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 不安全 | 安全(synchronized) |
| 性能 | 拼接慢 | 最快 | 比StringBuilder慢 |
| 底层 | char[]/byte[] | char[](可扩容) | char[](可扩容) |
| 扩容策略 | - | (旧容量 << 1) + 2 | 同 StringBuilder |
2.3 面向对象深度
2.3.1 多态的底层实现
- 方法表(vtable):每个类在方法区有一个虚方法表,存储各虚方法的实际入口地址
- 子类的方法表包含父类方法表的副本,覆写的方法指向子类的实现
- invokevirtual 指令执行时,通过对象的实际类型找到方法表,定位方法入口
- 内联缓存(Inline Cache):JIT 优化,缓存方法调用的目标地址
- 单态内联缓存:只缓存一种类型 → 直接调用
- 多态内联缓存:缓存多种类型 → 条件判断
- 超多态:退化为虚方法表查找
2.3.2 接口的默认方法(Java 8)
- 解决接口演化问题(向后兼容)
- 菱形继承冲突解决规则:
- 类中声明的方法优先级最高
- 子接口优先级高于父接口
- 无法确定时必须显式覆写
2.3.3 泛型底层(类型擦除)
- Java 泛型是编译期行为,运行时类型擦除
List<String>和List<Integer>运行时是同一个类List- 擦除后用
Object替代(有上界时用上界) - 桥接方法(Bridge Method):编译器生成,保证多态正确
- 不能做的事:
new T()(不知道实际类型)new T[](可用Array.newInstance)instanceof T- 基本类型作为类型参数
- 获取泛型类型信息:
ParameterizedType:通过反射获取泛型参数的实际类型- TypeToken 模式(Gson/Guava)
2.3.4 注解底层原理
- 注解本质是继承
java.lang.annotation.Annotation的接口 - 运行时注解通过动态代理生成代理对象
RetentionPolicy:SOURCE(编译期丢弃)/ CLASS(字节码保留,运行时不可见)/ RUNTIME(运行时反射可见)- 注解处理器(Annotation Processor):编译期处理注解,生成代码(如 Lombok、Dagger)
2.4 反射机制
2.4.1 反射的底层
Class.forName()→ 调用 native 方法通过类加载器加载类Method.invoke()的两种实现:- NativeMethodAccessorImpl:通过 JNI 调用,前 15 次使用(
-Dsun.reflect.inflationThreshold=15) - GeneratedMethodAccessorXxx:动态生成字节码的委派实现,超过阈值后切换,性能更好
- NativeMethodAccessorImpl:通过 JNI 调用,前 15 次使用(
2.4.2 反射的性能优化
- 缓存 Class/Method/Field 对象
setAccessible(true)跳过访问权限检查- 使用 MethodHandle(JDK 7+)替代反射
- 使用 LambdaMetafactory 生成 lambda 调用
2.5 异常体系
Throwable
├── Error(不应捕获)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── NoClassDefFoundError
│ └── ...
└── Exception
├── Checked Exception(编译时检查)
│ ├── IOException
│ ├── SQLException
│ ├── ClassNotFoundException
│ └── ...
└── RuntimeException(Unchecked)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ClassCastException
├── IllegalArgumentException
├── ConcurrentModificationException
└── ...
ClassNotFoundException vs NoClassDefFoundError:
- ClassNotFoundException:动态加载类时找不到(Class.forName / ClassLoader.loadClass)
- NoClassDefFoundError:编译时存在,运行时找不到(依赖缺失、静态初始化失败)
2.6 Java 8+ 新特性底层
Lambda 表达式底层
- 不是匿名内部类的语法糖
- 编译时生成
invokedynamic指令 - 运行时通过
LambdaMetafactory.metafactory()动态生成实现类 - 使用
ASM字节码框架生成,不会生成额外的 class 文件 - 比匿名内部类性能更好(避免了额外类加载和对象创建的开销)
Stream API 底层
- 基于
Spliterator(可分割迭代器) - 惰性求值:中间操作不执行,终止操作才触发
- 内部维护一个操作管道(Pipeline)
- 并行流基于 ForkJoinPool.commonPool()
- 注意:并行流不一定比串行快(数据量小、IO 密集型、线程安全问题)
Optional
- 避免 NPE 的容器类
- 不应用于字段类型和方法参数(序列化问题、语义不明确)
- 适合用于方法返回值
第三章 并发编程深度
3.1 Java 内存模型(JMM)
3.1.1 JMM 核心概念
- JMM 定义了线程和主内存之间的抽象关系
- 主内存(Main Memory):所有线程共享,对应堆中对象实例
- 工作内存(Working Memory):线程私有,对应 CPU 寄存器和高速缓存
- 线程对变量的操作必须在工作内存中进行,不能直接读写主内存
3.1.2 八大原子操作
| 操作 | 作用域 | 说明 |
|---|---|---|
| lock | 主内存 | 将变量标识为线程独占 |
| unlock | 主内存 | 释放变量的独占标识 |
| read | 主内存 | 将变量值从主内存传输到工作内存 |
| load | 工作内存 | 将 read 的值放入工作内存副本 |
| use | 工作内存 | 将值传递给执行引擎 |
| assign | 工作内存 | 将执行引擎的值赋给工作内存变量 |
| store | 工作内存 | 将值传输到主内存 |
| write | 主内存 | 将 store 的值写入主内存变量 |
3.1.3 happens-before 原则
- 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作
- 监视器锁规则:unlock happens-before 后续对同一把锁的 lock
- volatile 规则:volatile 写 happens-before 后续对同一 volatile 的读
- 线程启动规则:Thread.start() happens-before 该线程中的任何操作
- 线程终止规则:线程中所有操作 happens-before 其他线程检测到该线程终止
- 中断规则:线程 interrupt() 调用 happens-before 被中断线程检测到中断
- 终结器规则:构造函数完成 happens-before finalize() 开始
- 传递性:A happens-before B,B happens-before C → A happens-before C
3.1.4 指令重排序
- 编译器重排序:编译器在不改变单线程语义下重排语句
- 处理器重排序:CPU 流水线乱序执行
- 内存系统重排序:读写缓冲区导致的乱序
内存屏障(Memory Barrier):
- LoadLoad:确保前面的 Load 先于后面的 Load
- StoreStore:确保前面的 Store 先于后面的 Store
- LoadStore:确保前面的 Load 先于后面的 Store
- StoreLoad:全能型,确保前面的 Store 先于后面的 Load(开销最大)
3.2 volatile 深度
底层实现
- 写操作时加 Lock 前缀指令(x86 平台)
- Lock 前缀指令效果:
- 将当前处理器缓存行写回主内存
- 使其他 CPU 中缓存了该内存地址的数据无效(MESI 协议嗅探)
volatile 的语义
- 可见性:一个线程修改 volatile 变量后,其他线程立即可见
- 有序性:禁止指令重排(通过插入内存屏障)
- 不保证原子性:
volatile int count; count++不是原子操作
volatile 的内存屏障插入策略
- volatile 写前插入 StoreStore 屏障
- volatile 写后插入 StoreLoad 屏障
- volatile 读后插入 LoadLoad 屏障
- volatile 读后插入 LoadStore 屏障
典型应用
- DCL 单例模式中防止指令重排
- 状态标志位
- 一写多读场景
3.3 synchronized 深度
3.3.1 底层实现
- 代码块:编译后在同步块前后插入
monitorenter和monitorexit指令 - 方法:方法标志位设置
ACC_SYNCHRONIZED - 底层依赖操作系统的 Mutex Lock(重量级锁时)
3.3.2 Monitor 机制
- 每个 Java 对象都关联一个 Monitor(ObjectMonitor,C++ 实现)
- Monitor 结构:
- _owner:当前持有锁的线程
- _EntryList:等待获取锁的线程队列(阻塞)
- _WaitSet:调用 wait() 后进入的等待队列
- _count:重入计数器
- _recursions:重入次数
3.3.3 锁升级过程(JDK 6+)
无锁 → 偏向锁 → 轻量级锁 → 重量级锁(不可逆)
偏向锁(Biased Locking):
- 第一个获取锁的线程,在 Mark Word 中记录线程 ID
- 后续该线程再获取锁只需比对线程 ID,无需 CAS
- 另一个线程竞争时,撤销偏向锁,升级为轻量级锁
- 撤销需要在安全点进行
- JDK 15 默认关闭偏向锁(
-XX:-UseBiasedLocking)
轻量级锁:
- 在当前线程栈帧中创建 Lock Record
- 将 Mark Word 复制到 Lock Record(Displaced Mark Word)
- CAS 尝试将 Mark Word 更新为指向 Lock Record 的指针
- 成功则获取锁;失败则自旋重试
- 自旋超过阈值或有第三个线程竞争,升级为重量级锁
自适应自旋:
- 根据上次自旋是否成功动态调整自旋次数
- 成功过 → 增加自旋次数;从未成功 → 跳过自旋
重量级锁:
- 通过 Monitor 实现
- 未获取到锁的线程阻塞挂起(涉及用户态→内核态切换)
- 唤醒后竞争锁
3.3.4 锁优化技术
- 锁消除(Lock Elimination):JIT 通过逃逸分析消除不可能存在竞争的锁
- 锁粗化(Lock Coarsening):将连续的加锁解锁合并为一个更大范围的锁
- 锁降级:理论上重量级锁在 STW 期间可以降级(非常规操作)
3.4 AQS(AbstractQueuedSynchronizer)
3.4.1 核心设计
- state 变量:
volatile int state,表示同步状态 - CLH 队列变体:双向链表实现的 FIFO 等待队列
- Node 节点:封装线程信息和等待状态
- CANCELLED(1):取消等待
- SIGNAL(-1):后继节点需要被唤醒
- CONDITION(-2):在条件队列中等待
- PROPAGATE(-3):共享模式下传播唤醒
3.4.2 独占模式获取锁流程
acquire(1)→tryAcquire(1)(子类实现)- 获取失败 →
addWaiter(Node.EXCLUSIVE)加入 CLH 队列尾部(CAS) acquireQueued(node, 1)→ 自旋尝试获取锁- 前驱是 head 时尝试 tryAcquire
- 否则通过
shouldParkAfterFailedAcquire判断是否阻塞 - 需要阻塞时调用
LockSupport.park(this)
3.4.3 共享模式
acquireShared/releaseShared- 获取成功后传播唤醒后续共享节点(
setHeadAndPropagate) - 应用:Semaphore、CountDownLatch、ReadWriteLock 中的读锁
3.4.4 Condition 条件队列
- 每个 Condition 维护一个单向链表(条件队列)
await():释放锁 → 加入条件队列 → 阻塞signal():将条件队列头节点转移到 CLH 队列尾部- 比 Object.wait/notify 更灵活(多个等待队列)
3.4.5 基于 AQS 的实现
| 实现 | state 含义 | 模式 |
|---|---|---|
| ReentrantLock | 重入次数 | 独占 |
| ReentrantReadWriteLock | 高16位读锁计数,低16位写锁计数 | 共享+独占 |
| Semaphore | 许可数 | 共享 |
| CountDownLatch | 计数值 | 共享 |
3.5 ReentrantLock 深度
公平锁 vs 非公平锁
公平锁:
tryAcquire时先检查 CLH 队列中是否有前驱节点- 严格按照 FIFO 顺序获取锁
- 吞吐量较低(频繁线程切换)
非公平锁(默认):
- 新线程先 CAS 抢锁,失败才入队
- 可能导致线程饥饿
- 吞吐量更高(减少线程切换)
ReentrantLock vs synchronized
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 层(字节码指令) | API 层(Java 代码) |
| 锁获取 | 隐式,进入同步块自动获取 | 显式,lock()/unlock() |
| 可中断 | 不可中断 | lockInterruptibly() |
| 超时获取 | 不支持 | tryLock(timeout) |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | 一个(wait/notify) | 多个(Condition) |
| 锁绑定多个条件 | 不可以 | 可以(newCondition) |
| 释放 | 自动释放 | 必须手动 finally 中释放 |
| 锁状态查询 | 不支持 | isLocked(), getHoldCount() |
| 性能 | JDK 6 后优化接近 | 相当 |
3.6 线程池深度
3.6.1 核心参数
- corePoolSize:核心线程数(默认不会被回收,除非 allowCoreThreadTimeOut)
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程空闲存活时间
- workQueue:任务队列
- threadFactory:线程工厂
- handler:拒绝策略
3.6.2 执行流程
- 当前线程数 < corePoolSize → 创建核心线程执行
- 当前线程数 >= corePoolSize → 任务入队
- 队列满 && 线程数 < maximumPoolSize → 创建非核心线程执行
- 队列满 && 线程数 >= maximumPoolSize → 执行拒绝策略
3.6.3 工作队列类型
| 队列 | 特点 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue | 有界,数组实现,FIFO | 限制队列大小 |
| LinkedBlockingQueue | 可选有界(默认 Integer.MAX_VALUE),链表 | newFixedThreadPool |
| SynchronousQueue | 无容量,直接移交 | newCachedThreadPool |
| PriorityBlockingQueue | 无界,优先级排序 | 有优先级的任务 |
| DelayedWorkQueue | 延迟队列 | newScheduledThreadPool |
3.6.4 拒绝策略
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException |
| CallerRunsPolicy | 调用者线程执行任务 |
| DiscardPolicy | 直接丢弃 |
| DiscardOldestPolicy | 丢弃队列头部任务,重新提交 |
3.6.5 为什么不推荐 Executors 创建线程池?
newFixedThreadPool/newSingleThreadExecutor:LinkedBlockingQueue 无界队列,可能导致 OOMnewCachedThreadPool:maximumPoolSize 为 Integer.MAX_VALUE,可能创建大量线程导致 OOMnewScheduledThreadPool:同上问题- 应该使用 ThreadPoolExecutor 构造函数自定义参数
3.6.6 线程池大小如何确定?
- CPU 密集型:N + 1(N 为 CPU 核心数)
- IO 密集型:2N 或 N × (1 + W/C),W=等待时间,C=计算时间
- 实际场景需要压测调优
- 关注指标:CPU 利用率、线程数、队列大小、响应时间
3.6.7 线程池的线程复用原理
- Worker 线程 run() 方法中循环调用
getTask()从工作队列取任务 - 核心线程通过
workQueue.take()(阻塞等待)保持存活 - 非核心线程通过
workQueue.poll(keepAliveTime, TimeUnit)超时后退出 - 本质:线程不是直接执行 Runnable,而是循环从队列中取任务执行
3.7 并发工具类
3.7.1 CountDownLatch vs CyclicBarrier vs Semaphore
| 维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 作用 | 等待 N 个事件完成 | N 个线程互相等待到达屏障 | 控制并发访问数量 |
| 是否可重用 | 不可重用 | 可重用(reset) | 不涉及 |
| 底层 | AQS 共享模式 | ReentrantLock + Condition | AQS 共享模式 |
| 计数方向 | 递减(countDown) | 递增(await) | 递减/递增(acquire/release) |
| 典型场景 | 主线程等待子线程 | 多线程同时开始 | 限流、连接池 |
3.7.2 ConcurrentHashMap 深度
JDK 7 实现:
- Segment 数组 + HashEntry 数组 + 链表
- 分段锁(Segment 继承 ReentrantLock)
- 默认 16 个 Segment,并发度 16
- size() 先尝试不加锁统计两次,不一致再加锁
JDK 8 实现:
- Node 数组 + 链表/红黑树
- 取消 Segment,使用 CAS + synchronized(锁住链表头节点/红黑树根节点)
- 链表长度 >= 8 且数组长度 >= 64 时转红黑树
tabAt/casTabAt/setTabAt:Unsafe 直接操作内存- 扩容:多线程协助扩容(transferIndex 分配迁移区间)
- size():baseCount + CounterCell 数组(类似 LongAdder 分段计数)
- 为什么用 synchronized 替代 ReentrantLock?
- 锁粒度更细(锁住桶头节点 vs 锁住整个 Segment)
- synchronized 经过 JDK 优化后性能不亚于 ReentrantLock
- 减少内存开销(不需要额外的 Lock 对象)
3.7.3 CopyOnWriteArrayList
- 写时复制:修改操作复制底层数组,在新数组上修改,再替换引用
- 读操作无锁,写操作加 ReentrantLock
- 适合读多写少场景
- 缺点:内存占用大(复制)、数据一致性是最终一致性
3.7.4 ThreadLocal 深度
数据结构:
- 每个 Thread 持有一个 ThreadLocalMap
- ThreadLocalMap 以 ThreadLocal 实例为 key(弱引用),value 为线程本地值
- 底层是 Entry[] 数组,使用开放地址法(线性探测)解决哈希冲突
内存泄漏问题:
- ThreadLocalMap 的 key 是 WeakReference
- ThreadLocal 对象被回收后,key 变为 null,但 value 仍然强引用
- 解决:使用完必须调用
remove() - ThreadLocalMap 在 get/set/remove 时会清理 stale entry(expungeStaleEntry)
InheritableThreadLocal:
- 子线程可以继承父线程的 ThreadLocal 值
- 在 Thread 构造函数中从父线程的 inheritableThreadLocals 复制
- 线程池场景下不适用(线程复用导致值不更新) → TransmittableThreadLocal(阿里开源)
3.8 原子类与 CAS
3.8.1 CAS 原理
- Compare And Swap:比较并交换,CPU 级别的原子操作
- 操作数:内存值 V、预期值 A、新值 B
- 当 V==A 时,将 V 更新为 B,否则不做操作
- Java 中通过
Unsafe.compareAndSwapXxx实现,对应 CPU 的cmpxchg指令(x86)
3.8.2 CAS 的三个问题
- ABA 问题:值从 A→B→A,CAS 认为没变化
- 解决:AtomicStampedReference(版本号)/ AtomicMarkableReference(标记位)
- 自旋开销:长时间 CAS 失败导致 CPU 空转
- 解决:LongAdder 分段思想、退避策略
- 只能保证一个变量的原子操作
- 解决:AtomicReference 包装多个变量为对象
3.8.3 LongAdder vs AtomicLong
- AtomicLong:单一 value 上 CAS,高竞争下性能差
- LongAdder:base + Cell[] 数组分散竞争
- 低竞争:直接 CAS 更新 base
- 高竞争:hash 到不同 Cell 上更新
- sum() 时累加 base + 所有 Cell 的值(非实时精确)
- LongAdder 写性能远优于 AtomicLong,但 sum() 非精确
3.9 Fork/Join 框架
- 分治思想:将大任务分割为小任务,最终合并结果
- 工作窃取算法(Work-Stealing):每个线程维护双端队列,空闲线程从其他线程队列的尾部窃取任务
- ForkJoinPool → ForkJoinWorkerThread → ForkJoinTask
- RecursiveTask(有返回值)/ RecursiveAction(无返回值)
- Java 8 的 parallelStream 底层基于 ForkJoinPool.commonPool()
第四章 集合框架底层
4.1 ArrayList 底层
- 基于
Object[]数组 - 默认初始容量:10(JDK 7 在构造时创建;JDK 8 懒初始化,首次 add 时分配)
- 扩容:
newCapacity = oldCapacity + (oldCapacity >> 1)(1.5 倍) - 使用
Arrays.copyOf()→System.arraycopy()(native 方法)复制 modCount实现 fail-fast 机制- 随机访问 O(1),增删 O(n)(需要移动元素)
4.2 LinkedList 底层
- 基于双向链表(JDK 6 之前是循环链表)
- 同时实现了 List 和 Deque 接口
- 每个节点维护 prev/next 指针
- 随机访问 O(n)(虽然内部优化了从头或尾遍历),增删 O(1)(在已知节点位置时)
- 不推荐使用(缓存不友好,内存开销大)
4.3 HashMap 深度
4.3.1 JDK 8 数据结构
- Node[] 数组 + 链表 + 红黑树
- 链表长度 >= 8 且 数组长度 >= 64 → 转红黑树
- 红黑树节点 <= 6 → 退化为链表
- 为什么是 8? 泊松分布下,链表长度达到 8 的概率约为 0.00000006,几乎不可能
4.3.2 hash 计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 高 16 位异或低 16 位(扰动函数):让高位也参与索引计算,减少碰撞
- 定位桶:
(n - 1) & hash(n 为数组长度,必须是 2 的幂)
4.3.3 put 流程
- 计算 key 的 hash
- 如果 table 为 null 或 length == 0 → resize() 初始化
- 计算桶位
(n-1) & hash - 桶位为空 → 直接放入新节点
- 桶位不为空:
- 首节点 key 相同 → 覆盖
- 首节点是 TreeNode → 红黑树插入
- 链表遍历:找到相同 key 则覆盖;否则尾插法插入,判断是否需要树化
- 判断
++size > threshold→ resize()
4.3.4 resize 扩容
- 新容量 = 旧容量 × 2
- JDK 8 扩容优化:无需重新计算 hash
hash & oldCap == 0→ 位置不变(低位链表)hash & oldCap != 0→ 位置 = 原位置 + oldCap(高位链表)- 原理:扩容后 n-1 多了一个最高位 1,hash 该位为 0 或 1 决定位置
4.3.5 线程安全问题
- JDK 7:头插法在并发扩容时可能形成环形链表 → 死循环
- JDK 8:尾插法不会形成环,但仍可能丢失数据
- 不要在多线程下使用 HashMap
4.3.6 为什么容量必须是 2 的幂?
(n-1) & hash等价于hash % n,位运算更快- 保证哈希值能均匀分布到各桶
- 扩容时的优化依赖于此
4.4 TreeMap
- 基于红黑树实现的有序 Map
- key 需要实现 Comparable 或传入 Comparator
- 增删查改 O(log n)
- 不允许 null key(需要比较)
4.5 LinkedHashMap
- HashMap + 双向链表维护插入顺序(或访问顺序)
accessOrder = true时按访问顺序排列(LRU 基础)- 重写
removeEldestEntry可实现简单的 LRU Cache
4.6 HashSet
- 底层是 HashMap,value 是一个固定的
PRESENT = new Object() - add → HashMap.put(e, PRESENT)
- 判重依赖 hashCode() + equals()
4.7 PriorityQueue
- 基于最小堆(完全二叉树,数组实现)
- 入队:上浮(siftUp)
- 出队:下沉(siftDown)
- 非线程安全(线程安全版 PriorityBlockingQueue)
第五章 IO/NIO/AIO 深度
5.1 IO 模型
5.1.1 五种 IO 模型(Unix)
| 模型 | 特点 | 阻塞情况 |
|---|---|---|
| 阻塞 IO(BIO) | 等待数据+拷贝数据都阻塞 | 全程阻塞 |
| 非阻塞 IO | 轮询检查数据是否就绪 | 拷贝数据时阻塞 |
| IO 多路复用 | select/poll/epoll 监控多个 fd | select 调用阻塞 |
| 信号驱动 IO | 数据就绪时内核发信号 | 拷贝数据时阻塞 |
| 异步 IO(AIO) | 全程不阻塞,完成时回调 | 不阻塞 |
5.1.2 select vs poll vs epoll
| 维度 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | bitmap(fd_set) | 链表(pollfd) | 红黑树+就绪链表 |
| fd 上限 | 1024(FD_SETSIZE) | 无限制 | 无限制 |
| fd 传递 | 每次调用全量复制到内核 | 每次调用全量复制 | epoll_ctl 添加,无需每次复制 |
| 触发方式 | 水平触发 | 水平触发 | 水平触发 + 边缘触发 |
| 遍历方式 | 线性遍历所有 fd | 线性遍历所有 fd | 只遍历就绪的 fd |
| 时间复杂度 | O(n) | O(n) | O(1) |
epoll 关键系统调用:
epoll_create:创建 epoll 实例epoll_ctl:添加/修改/删除 fd 到红黑树epoll_wait:等待就绪事件
水平触发(LT)vs 边缘触发(ET):
- LT:只要 fd 有数据可读,epoll_wait 每次都会返回(可能重复通知)
- ET:fd 状态变化时才通知一次(必须一次读完,否则遗漏)
5.2 Java BIO
- 基于流(Stream)的单向传输
- 一个连接对应一个线程
- InputStream/OutputStream → 字节流
- Reader/Writer → 字符流
- 装饰者模式:BufferedInputStream 包装 FileInputStream
5.3 Java NIO
核心组件
Buffer(缓冲区):
- capacity ≥ limit ≥ position ≥ 0
flip():写→读切换(limit=position, position=0)clear():读→写切换(position=0, limit=capacity)compact():读→写切换(保留未读数据)- DirectByteBuffer vs HeapByteBuffer
Channel(通道):
- 双向,可以同时读写
- FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel
- transferTo/transferFrom:零拷贝
Selector(选择器):
- 一个线程管理多个 Channel
- SelectionKey:OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE
- select() 阻塞直到有事件就绪
- 底层对应 epoll/kqueue/IOCP
零拷贝
传统 IO 的数据拷贝:
- 磁盘 → 内核缓冲区(DMA 拷贝)
- 内核缓冲区 → 用户缓冲区(CPU 拷贝)
- 用户缓冲区 → Socket 缓冲区(CPU 拷贝)
- Socket 缓冲区 → 网卡(DMA 拷贝)
- 4 次拷贝,4 次上下文切换
sendfile(零拷贝):
- 磁盘 → 内核缓冲区(DMA 拷贝)
- 内核缓冲区 → Socket 缓冲区(CPU 拷贝 / DMA gather)
- Socket 缓冲区 → 网卡(DMA 拷贝)
- 最少 2 次拷贝(DMA gather 时),2 次上下文切换
mmap(内存映射):
- 磁盘 → 内核缓冲区(DMA 拷贝)
- 用户空间映射到内核缓冲区(无拷贝)
- 内核缓冲区 → Socket 缓冲区(CPU 拷贝)
- Socket 缓冲区 → 网卡(DMA 拷贝)
- 3 次拷贝,4 次上下文切换
Java 中的零拷贝:
FileChannel.transferTo()→ sendfileMappedByteBuffer→ mmap- Netty 的
CompositeByteBuf:逻辑上合并多个 Buffer,避免数据拷贝
5.4 Java AIO(NIO.2)
- 基于回调或 Future 的异步模型
- AsynchronousServerSocketChannel / AsynchronousSocketChannel
- CompletionHandler 回调接口
- 底层:Linux 上用 epoll 模拟(不是真正的 AIO),Windows 上用 IOCP
- 实际应用不多,Netty 也放弃了 AIO 支持
第六章 Spring 全家桶底层原理
6.1 Spring IoC 容器
6.1.1 IoC 核心流程
- 资源定位:通过 ResourceLoader 定位 Bean 定义资源(XML/注解/JavaConfig)
- Bean 定义加载:BeanDefinitionReader 读取并解析为 BeanDefinition
- Bean 定义注册:注册到 BeanDefinitionRegistry(底层 ConcurrentHashMap)
- Bean 实例化与初始化:
- 实例化(Instantiation):反射创建对象 / FactoryMethod
- 属性注入(Populate):依赖注入
- 初始化(Initialization):Aware 回调 → BeanPostProcessor.postProcessBeforeInitialization → InitializingBean.afterPropertiesSet / init-method → BeanPostProcessor.postProcessAfterInitialization
6.1.2 Bean 的完整生命周期
- 实例化(构造方法或工厂方法)
- 属性注入(setter、@Autowired 等)
- BeanNameAware.setBeanName()
- BeanFactoryAware.setBeanFactory()
- ApplicationContextAware.setApplicationContext()
- BeanPostProcessor.postProcessBeforeInitialization()
- @PostConstruct
- InitializingBean.afterPropertiesSet()
- 自定义 init-method
- BeanPostProcessor.postProcessAfterInitialization()(AOP 代理在此创建)
- Bean 就绪,使用中
- @PreDestroy
- DisposableBean.destroy()
- 自定义 destroy-method
6.1.3 循环依赖解决(三级缓存)
- 一级缓存 singletonObjects:完全初始化的 Bean(ConcurrentHashMap)
- 二级缓存 earlySingletonObjects:提前暴露的 Bean(未完成属性注入,HashMap)
- 三级缓存 singletonFactories:ObjectFactory(lambda,延迟创建代理,HashMap)
解决流程(A 依赖 B,B 依赖 A):
- 创建 A → 实例化 → 将 ObjectFactory(A) 放入三级缓存
- A 属性注入 → 发现需要 B → 创建 B
- 创建 B → 实例化 → 将 ObjectFactory(B) 放入三级缓存
- B 属性注入 → 发现需要 A → 从三级缓存获取 ObjectFactory(A) → 调用 getObject() → 得到 A 的早期引用(可能是代理)→ 放入二级缓存,删除三级缓存
- B 完成初始化 → 放入一级缓存
- A 拿到 B → 完成初始化 → 放入一级缓存
为什么需要三级缓存而不是两级?
- 如果没有 AOP,两级缓存就够了
- 三级缓存的 ObjectFactory 负责在需要时创建代理对象
- 确保无论何时获取 Bean,得到的都是正确的代理对象
- 延迟代理创建,如果没有循环依赖,代理在初始化后创建
哪些情况无法解决循环依赖?
- 构造器注入:实例化阶段就需要依赖,此时还没来得及放入缓存
- prototype 作用域:不缓存 Bean
- @Async:因为后置处理器创建了新的代理对象
- 解决方案:@Lazy、setter 注入、@DependsOn
6.1.4 BeanFactory vs ApplicationContext
| 维度 | BeanFactory | ApplicationContext |
|---|---|---|
| Bean 加载 | 懒加载 | 预加载(启动时实例化单例) |
| 功能 | 基础 IoC | IoC + AOP + 事件 + 国际化 + 资源访问 |
| 实现 | DefaultListableBeanFactory | ClassPathXmlApplicationContext / AnnotationConfigApplicationContext 等 |
| 后处理器注册 | 手动 | 自动检测并注册 |
6.2 Spring AOP 深度
6.2.1 代理方式
JDK 动态代理:
- 基于接口
java.lang.reflect.Proxy.newProxyInstance()- InvocationHandler.invoke()
- 运行时动态生成 $Proxy0 类(继承 Proxy,实现目标接口)
CGLIB 代理:
- 基于继承(子类代理)
- 使用 ASM 字节码框架生成子类
- MethodInterceptor.intercept()
- 无法代理 final 类/方法
- Spring 5 默认使用 CGLIB(
spring.aop.proxy-target-class=true)
JDK 代理 vs CGLIB:
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 限制 | 目标必须实现接口 | 不能代理 final |
| 生成方式 | 反射 | ASM 字节码 |
| 性能 | JDK 8+ 优化后接近 | 生成慢,调用快(JDK 8 后差距不大) |
| 场景 | 有接口 | 无接口或需要代理具体类 |
6.2.2 AOP 执行链
- 基于责任链模式
- MethodInterceptor 链(ExposeInvocationInterceptor → AspectJAroundAdvice → MethodBeforeAdviceInterceptor → AspectJAfterAdvice → AfterReturningAdviceInterceptor → AfterThrowingAdviceInterceptor)
ReflectiveMethodInvocation.proceed()依次调用链中的 Interceptor
6.2.3 AOP 通知执行顺序
- @Around 前半部分
- @Before
- 目标方法
- @AfterReturning / @AfterThrowing
- @After
- @Around 后半部分
多个切面的顺序通过 @Order 控制
6.3 Spring MVC 流程
- DispatcherServlet 接收请求
- HandlerMapping 查找 Handler(Controller 方法)
- HandlerAdapter 适配并执行 Handler
- Handler 返回 ModelAndView
- ViewResolver 解析视图
- View 渲染响应
DispatcherServlet 核心方法
doDispatch():核心分发逻辑getHandler():遍历 HandlerMapping 找到匹配的 Handler + 拦截器链getHandlerAdapter():找到支持该 Handler 的 Adapter- 拦截器执行:preHandle → Handler → postHandle → afterCompletion
@RequestBody 参数解析
- HttpMessageConverter 负责消息转换
MappingJackson2HttpMessageConverter→ JSON 转对象RequestResponseBodyMethodProcessor→ 参数解析 + 返回值处理
6.4 Spring Boot 自动配置原理
@SpringBootApplication 分解
@SpringBootConfiguration:标记配置类@EnableAutoConfiguration:开启自动配置@ComponentScan:组件扫描
自动配置流程
@EnableAutoConfiguration→@Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector.selectImports()- 通过
SpringFactoriesLoader.loadFactoryNames()读取META-INF/spring.factories - 加载所有
EnableAutoConfiguration对应的配置类 - 通过
@Conditional系列注解条件过滤(@ConditionalOnClass、@ConditionalOnMissingBean等)
@Conditional 系列
| 注解 | 条件 |
|---|---|
| @ConditionalOnClass | classpath 中存在指定类 |
| @ConditionalOnMissingClass | classpath 中不存在指定类 |
| @ConditionalOnBean | 容器中存在指定 Bean |
| @ConditionalOnMissingBean | 容器中不存在指定 Bean |
| @ConditionalOnProperty | 指定配置属性有指定值 |
| @ConditionalOnWebApplication | 是 Web 应用 |
6.5 Spring 事务
6.5.1 事务传播行为
| 传播行为 | 含义 |
|---|---|
| REQUIRED(默认) | 有事务加入,没有新建 |
| SUPPORTS | 有事务加入,没有以非事务运行 |
| MANDATORY | 必须在事务中,否则抛异常 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 |
| NOT_SUPPORTED | 以非事务运行,挂起当前事务 |
| NEVER | 以非事务运行,有事务则抛异常 |
| NESTED | 有事务则嵌套(保存点),没有则新建 |
6.5.2 @Transactional 失效场景
- 方法非 public:Spring AOP 默认只代理 public 方法
- 自调用:同一类中方法调用不经过代理(
this.method()不走 AOP) - 异常被捕获:catch 吞掉异常,事务无法感知
- rollbackFor 不匹配:默认只对 RuntimeException 和 Error 回滚
- 传播行为不当:SUPPORTS 等可能导致无事务
- 多线程:事务基于 ThreadLocal,新线程不在事务中
- 非 Spring 管理的 Bean:new 出来的对象没有代理
- 数据库引擎不支持:如 MyISAM 不支持事务
6.5.3 NESTED vs REQUIRES_NEW
| 维度 | NESTED | REQUIRES_NEW |
|---|---|---|
| 本质 | 保存点(savepoint) | 独立事务 |
| 外部回滚 | 一起回滚 | 不影响(已提交) |
| 内部回滚 | 回滚到保存点,外部可继续 | 不影响外部 |
| 数据库要求 | 需要支持 savepoint | 任何支持事务的数据库 |
6.6 Spring Security
核心架构
- SecurityFilterChain:一组 Filter 组成的安全过滤链
- 核心 Filter:
- UsernamePasswordAuthenticationFilter:表单登录
- BasicAuthenticationFilter:HTTP Basic 认证
- FilterSecurityInterceptor:授权决策
- ExceptionTranslationFilter:异常处理
认证流程
- AuthenticationFilter 拦截请求
- 创建 Authentication(UsernamePasswordAuthenticationToken)
- AuthenticationManager.authenticate() → 委托给 AuthenticationProvider
- AuthenticationProvider 调用 UserDetailsService.loadUserByUsername()
- 验证密码(PasswordEncoder)
- 认证成功 → SecurityContextHolder 保存 Authentication
第七章 MyBatis 底层原理
7.1 核心架构
7.1.1 核心组件
- SqlSessionFactory:创建 SqlSession 的工厂
- SqlSession:一次数据库会话,非线程安全
- Executor:SQL 执行器
- SimpleExecutor:每次执行都创建新的 Statement
- ReuseExecutor:复用 Statement
- BatchExecutor:批量执行
- CachingExecutor:二级缓存装饰器
- MappedStatement:一个 SQL 映射语句的封装
- StatementHandler:处理 JDBC Statement
- ParameterHandler:处理参数
- ResultSetHandler:处理结果集
- TypeHandler:Java 类型与 JDBC 类型转换
7.1.2 SQL 执行流程
- SqlSession 调用 Executor
- Executor 调用 StatementHandler
- StatementHandler → ParameterHandler 设置参数
- StatementHandler 执行 SQL
- ResultSetHandler 处理结果集 → TypeHandler 转换类型
- 返回结果
7.2 MyBatis 缓存
一级缓存
- SqlSession 级别,默认开启
- 底层 HashMap(PerpetualCache)
- CacheKey:MappedStatement id + offset + limit + SQL + 参数 + 环境
- 失效条件:
- 不同 SqlSession
- 同一 SqlSession 执行了 insert/update/delete
- 手动清除 clearCache()
- 配置了 flushCache=true
- Spring 整合后的注意事项:每次 Mapper 方法调用创建新 SqlSession,一级缓存基本失效
二级缓存
- namespace 级别,跨 SqlSession 共享
- 需要显式开启(
<cache/>或 @CacheNamespace) - 提交事务后才生效(TransactionalCache)
- 序列化存储(对象需实现 Serializable)
- 问题:多表查询时可能读到脏数据(只能感知本 namespace 的更新)
- 不推荐使用,建议用 Redis 等外部缓存
7.3 Mapper 代理原理
- Mapper 接口没有实现类,MyBatis 通过JDK 动态代理生成实现
MapperProxyFactory.newInstance()→Proxy.newProxyInstance()MapperProxy.invoke()→ 根据方法信息找到 MappedStatement → 调用 SqlSession 对应方法MapperMethod:封装了 SQL 命令类型和方法签名
7.4 MyBatis 插件(拦截器)
- 基于责任链模式 + JDK 动态代理
- 可拦截的四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler
Interceptor.intercept(Invocation)→Invocation.proceed()- 典型应用:分页插件 PageHelper、SQL 打印、数据权限
7.5 #{ } vs ${ }
| 维度 | #{} | ${} |
|---|---|---|
| 处理方式 | PreparedStatement 参数占位符 | 字符串拼接(直接替换) |
| SQL 注入 | 防止 | 不防止 |
| 使用场景 | 参数值 | 表名、列名、ORDER BY |
| 预编译 | 是 | 否 |
第八章 中间件原理与八股文
8.1 Redis 深度
8.1.1 数据结构底层
String:
- 底层 SDS(Simple Dynamic String)
- 特点:O(1) 获取长度、二进制安全、空间预分配、惰性释放
- 编码:int(纯数字)/ embstr(≤44字节)/ raw(>44字节)
List:
- JDK 3.2+:quicklist(双向链表 + ziplist/listpack)
- ziplist:连续内存,节省空间但修改可能触发连锁更新
- listpack(Redis 7.0):解决 ziplist 连锁更新问题
Hash:
- 小数据量:ziplist/listpack
- 大数据量:hashtable
- 渐进式 rehash:维护两个 hash 表,逐步迁移
Set:
- 整数且数量少:intset
- 其他:hashtable
Sorted Set(ZSet):
- skiplist(跳表) + hashtable
- skiplist:多层链表,查找 O(log n)
- 为什么不用红黑树?范围查询更方便、实现简单、并发友好
8.1.2 持久化
RDB(Redis Database):
- fork 子进程生成快照(COW 机制)
- 二进制文件,恢复速度快
- 可能丢失最后一次快照后的数据
bgsave:后台异步保存
AOF(Append Only File):
- 记录每一条写命令
- 三种同步策略:always / everysec(默认) / no
- AOF 重写:
bgrewriteaof,重新生成紧凑的 AOF 文件 - Redis 7.0 引入 Multi Part AOF
混合持久化(Redis 4.0+):
- AOF 重写时前半段为 RDB 格式,后半段为 AOF 格式
- 兼顾启动速度和数据完整性
8.1.3 过期策略
- 惰性删除:访问时检查是否过期
- 定期删除:每次随机抽取一批 key 检查,过期比例 > 25% 则继续
8.1.4 内存淘汰策略
| 策略 | 范围 | 行为 |
|---|---|---|
| noeviction | - | 拒绝写入 |
| allkeys-lru | 所有 key | LRU 淘汰 |
| allkeys-lfu | 所有 key | LFU 淘汰 |
| allkeys-random | 所有 key | 随机淘汰 |
| volatile-lru | 有过期时间的 key | LRU 淘汰 |
| volatile-lfu | 有过期时间的 key | LFU 淘汰 |
| volatile-random | 有过期时间的 key | 随机淘汰 |
| volatile-ttl | 有过期时间的 key | 淘汰 TTL 最小的 |
Redis LRU 实现:近似 LRU,不是精确 LRU。每个 key 记录最后访问时间戳,淘汰时随机采样 N 个 key(默认5),淘汰最久未使用的
Redis LFU 实现(Redis 4.0+):基于 Morris counter 的概率计数器 + 衰减机制
8.1.5 缓存问题
缓存穿透:查询不存在的数据,请求直达数据库
- 解决:布隆过滤器、缓存空值(设短 TTL)、接口层校验
缓存击穿:热点 key 过期瞬间,大量请求打到数据库
- 解决:互斥锁(setnx)、逻辑过期、热点 key 永不过期
缓存雪崩:大量 key 同时过期或 Redis 宕机
- 解决:随机过期时间、Redis 集群高可用、多级缓存、限流降级
8.1.6 Redis 单线程为什么快?
- 纯内存操作
- 单线程避免上下文切换和锁竞争
- IO 多路复用(epoll)
- 高效的数据结构
- 注意:Redis 6.0 引入多线程 IO(网络 IO 多线程,命令执行仍单线程)
8.1.7 Redis 集群方案
主从复制:
- 全量同步:RDB + 增量缓冲区
- 增量同步:基于 replication offset + repl_backlog
哨兵(Sentinel):
- 监控、通知、自动故障转移
- 选举:先选 Sentinel Leader(Raft),再选新 Master(优先级→offset→runid)
Redis Cluster:
- 16384 个哈希槽(slot),CRC16(key) % 16384
- 去中心化,Gossip 协议通信
- 客户端重定向(MOVED / ASK)
- 故障检测:PFAIL → FAIL(半数以上 Master 认为 PFAIL)
8.1.8 分布式锁
基本实现:SET key value NX EX timeout
问题及解决:
- 锁过期但业务未完成 → 看门狗续期(Redisson)
- 主从切换锁丢失 → RedLock 算法(向 N 个独立 Redis 节点加锁,多数成功则获取锁)
- 可重入 → Hash 结构存线程标识和重入次数(Redisson)
- 公平锁 → Redisson 的 RedissonFairLock
8.2 消息队列
8.2.1 Kafka 深度
架构:Producer → Broker → Consumer(Consumer Group)
核心设计:
- 分区(Partition):Topic 的子单元,有序,分布在不同 Broker
- 副本(Replica):Leader + Follower,ISR 机制
- ISR(In-Sync Replicas):与 Leader 保持同步的副本集合
- 消费者组:一个分区只能被组内一个消费者消费
高性能原因:
- 顺序写磁盘(追加写 log 文件)
- 零拷贝(sendfile)
- 分区并行
- 批量发送和压缩
- Page Cache 利用操作系统缓存
- 稀疏索引(.index 文件)
消息可靠性:
- Producer:acks=0/1/all(-1)
- Broker:副本机制 + ISR + min.insync.replicas
- Consumer:手动提交 offset
消息顺序性:
- 单分区有序,多分区无序
- 保证顺序:相同 key 路由到同一分区 + max.in.flight.requests.per.connection=1
重复消费:
- Consumer 提交 offset 前宕机 → 重启后重新消费
- 解决:幂等性(数据库唯一索引、Redis 去重、幂等 token)
8.2.2 RocketMQ 深度
架构:NameServer + Broker + Producer + Consumer
特性:
- 事务消息:半消息 → 执行本地事务 → 提交/回滚 → 回查
- 延迟消息:指定延迟级别
- 死信队列:消费失败超过重试次数
- 消息过滤:Tag / SQL92
与 Kafka 的区别:
| 维度 | Kafka | RocketMQ |
|---|---|---|
| 设计目标 | 高吞吐 | 可靠性+功能丰富 |
| 事务消息 | 有(但不同) | 原生支持 |
| 延迟消息 | 不支持(需自实现) | 原生支持 |
| 消息回溯 | 按 offset | 按时间戳 |
| 刷盘 | 异步 | 同步/异步 |
| 注册中心 | ZooKeeper/KRaft | NameServer |
| 消息查询 | 不支持 | 支持(MessageId/Key) |
8.3 Netty 深度
8.3.1 线程模型
Reactor 模式:
- 单 Reactor 单线程:接收和处理都在一个线程
- 单 Reactor 多线程:一个线程接收,线程池处理
- 主从 Reactor 多线程:Boss 线程组接收连接,Worker 线程组处理 IO(Netty 默认)
Netty 线程模型:
- BossGroup:NioEventLoopGroup,负责 Accept 连接
- WorkerGroup:NioEventLoopGroup,负责读写
- 每个 NioEventLoop 包含一个 Selector + TaskQueue + 一个线程
- Channel 绑定到固定的 NioEventLoop,保证线程安全
8.3.2 核心组件
- Channel:网络通信的通道
- EventLoop:处理 IO 事件的循环
- ChannelPipeline:ChannelHandler 的双向链表(责任链)
- ChannelHandler:Inbound(入站)/ Outbound(出站)处理器
- ByteBuf:增强的 Buffer(读写分离的双指针、池化、引用计数)
8.3.3 ByteBuf
- 读写双指针:readerIndex / writerIndex,无需 flip()
- 内存类型:Heap(堆内存)/ Direct(直接内存)/ Composite(复合)
- 池化:PooledByteBufAllocator(jemalloc 算法),减少 GC 压力
- 引用计数:
retain()/release(),0 时释放内存
8.3.4 粘包/拆包
- 原因:TCP 是字节流协议,没有消息边界
- 解决方案(Netty Decoder):
- FixedLengthFrameDecoder:固定长度
- LineBasedFrameDecoder:换行符分割
- DelimiterBasedFrameDecoder:自定义分隔符
- LengthFieldBasedFrameDecoder:长度字段(最常用)
8.3.5 Netty 零拷贝
- CompositeByteBuf:逻辑合并多个 Buffer
- slice():共享底层数据不拷贝
- FileRegion:封装 transferTo()
- 堆外内存减少一次拷贝
8.4 Dubbo
8.4.1 核心架构
- Provider → Registry → Consumer → Monitor
- 注册中心:ZooKeeper / Nacos
- 通信协议:dubbo(默认)/ http / grpc / rest
8.4.2 服务调用流程
- Consumer 从 Registry 获取 Provider 列表
- 通过负载均衡选择 Provider
- 通过 Netty 发送序列化后的请求
- Provider 反序列化 → 调用本地服务 → 返回结果
8.4.3 SPI 机制
- Java SPI:
ServiceLoader.load(),一次加载所有实现 - Dubbo SPI:
ExtensionLoader,按需加载(key=value 配置),支持 AOP(Wrapper)和 IoC(setter 注入)
8.4.4 负载均衡
| 策略 | 原理 |
|---|---|
| RandomLoadBalance | 加权随机 |
| RoundRobinLoadBalance | 加权轮询 |
| LeastActiveLoadBalance | 最少活跃调用数 |
| ConsistentHashLoadBalance | 一致性哈希 |
| ShortestResponseLoadBalance | 最短响应时间 |
8.4.5 容错策略
| 策略 | 行为 |
|---|---|
| Failover | 失败自动切换(默认,重试其他服务器) |
| Failfast | 快速失败(只调用一次) |
| Failsafe | 安全失败(忽略异常) |
| Failback | 失败自动恢复(定时重试) |
| Forking | 并行调用多个,一个成功即返回 |
| Broadcast | 广播调用所有 Provider |
8.5 Elasticsearch
8.5.1 核心概念
- 倒排索引(Inverted Index):词项 → 文档列表。底层使用 FST(有限状态转换器)压缩存储
- 分段(Segment):不可变的 Lucene 索引,写入后不能修改
- 近实时搜索:写入到可搜索有约 1 秒延迟(refresh_interval)
8.5.2 写入流程
- 写入请求到协调节点
- 路由到对应分片(_id hash % 分片数)
- 写入 Translog(预写日志)
- 写入 Index Buffer
- Refresh:Index Buffer → Segment(内存中,可搜索)
- Flush:Segment → 磁盘,清空 Translog
8.5.3 查询流程
- Query Phase:协调节点向所有分片发送查询,各分片返回 Top N 文档 ID + 分数
- Fetch Phase:协调节点合并排序后,向相关分片获取文档详情
第九章 数据库与 ORM 深度
9.1 MySQL 底层
9.1.1 InnoDB 架构
Buffer Pool:
- 缓存数据页和索引页,减少磁盘 IO
- LRU 链表(改进版:young + old 区域,防止全表扫描污染)
- Change Buffer:非唯一二级索引的写缓冲,减少随机 IO
- Adaptive Hash Index:自适应哈希索引
Redo Log:
- 物理日志,记录数据页的修改
- WAL(Write-Ahead Logging):先写日志再写数据
- 固定大小,循环写入(write pos + checkpoint)
- 保证持久性(Durability)
Undo Log:
- 逻辑日志,记录反向操作
- 支持事务回滚
- MVCC 的版本链基础
- purge 线程清理不需要的 undo log
Binlog(MySQL Server 层):
- 逻辑日志,记录 SQL 语句或行变更
- 用于主从复制和数据恢复
- 格式:Statement / Row / Mixed
两阶段提交(Redo Log + Binlog):
- Redo Log prepare
- 写入 Binlog
- Redo Log commit
- 保证 Redo Log 和 Binlog 的一致性
9.1.2 索引深度
B+ 树:
- 非叶子节点只存 key,不存 data → 每页能存更多 key → 树更矮
- 叶子节点包含所有数据,通过双向链表相连 → 范围查询友好
- 3 层 B+ 树(16KB 页,bigint 主键)可存约 2000 万行
- 计算:16KB / (8B + 6B) ≈ 1170 个指针 → 1170 × 1170 × 16 ≈ 2190 万
聚簇索引 vs 非聚簇索引:
| 维度 | 聚簇索引 | 非聚簇索引(二级索引) |
|---|---|---|
| 叶子节点 | 完整行数据 | 主键值 |
| 数量 | 只能有一个 | 可以有多个 |
| 回表 | 不需要 | 可能需要(通过主键查聚簇索引) |
覆盖索引:查询的字段都在索引中,无需回表
索引下推(ICP,Index Condition Pushdown):
- MySQL 5.6+
- 将 WHERE 条件中属于索引的部分下推到存储引擎层判断
- 减少回表次数
最左前缀原则:联合索引 (a, b, c)
- 能用到索引:a / a,b / a,b,c / a,c(a 全匹配,c 用于过滤)
- 不能用到索引:b / c / b,c
索引失效场景:
- 对索引列使用函数/运算
- 隐式类型转换
- LIKE 以 % 开头
- OR 条件中有非索引列
- 不满足最左前缀原则
- 优化器判断全表扫描更快
9.1.3 事务与锁
事务隔离级别:
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✓ | ✓ | ✓ |
| READ COMMITTED | ✗ | ✓ | ✓ |
| REPEATABLE READ(InnoDB 默认) | ✗ | ✗ | ✗(MVCC+间隙锁) |
| SERIALIZABLE | ✗ | ✗ | ✗ |
MVCC(多版本并发控制):
- 每行数据有隐藏字段:DB_TRX_ID(事务ID)+ DB_ROLL_PTR(回滚指针)+ DB_ROW_ID
- Undo Log 版本链:通过 DB_ROLL_PTR 串联历史版本
- ReadView(读视图):
- m_ids:活跃事务 ID 列表
- min_trx_id:最小活跃事务 ID
- max_trx_id:下一个要分配的事务 ID
- creator_trx_id:创建 ReadView 的事务 ID
- 可见性判断:
- trx_id < min_trx_id → 可见
- trx_id >= max_trx_id → 不可见
- min_trx_id <= trx_id < max_trx_id → 判断是否在 m_ids 中
- RC vs RR 的区别:RC 每次 SELECT 都创建新 ReadView;RR 只在第一次 SELECT 时创建
InnoDB 锁类型:
| 锁 | 说明 |
|---|---|
| 行锁(Record Lock) | 锁定索引记录 |
| 间隙锁(Gap Lock) | 锁定索引记录之间的间隙(防止幻读) |
| 临键锁(Next-Key Lock) | 行锁 + 间隙锁(左开右闭区间) |
| 意向锁(Intention Lock) | 表级锁,表示事务打算对行加什么锁 |
| 自增锁(AUTO-INC Lock) | 自增列的表级锁 |
| 插入意向锁 | 间隙锁的一种,允许不同事务在同一间隙插入不同数据 |
死锁:
- 检测:等待图(wait-for graph)检测环路
- 处理:选择代价最小的事务回滚
- 预防:按固定顺序访问资源、减小事务粒度、设置超时
9.1.4 日志系统对比
| 日志 | 层级 | 内容 | 作用 |
|---|---|---|---|
| Redo Log | InnoDB 引擎层 | 物理修改(数据页) | 崩溃恢复(持久性) |
| Undo Log | InnoDB 引擎层 | 逻辑回滚操作 | 事务回滚 + MVCC |
| Binlog | MySQL Server 层 | SQL/行变更 | 主从复制 + 数据恢复 |
| Slow Query Log | MySQL Server 层 | 慢 SQL | 性能分析 |
9.2 分库分表
拆分策略
- 垂直分库:按业务拆分(用户库、订单库、商品库)
- 垂直分表:按字段拆分(常用字段和不常用字段分开)
- 水平分库:相同结构,按规则分散数据到不同库
- 水平分表:相同结构,按规则分散数据到不同表
分片算法
- Hash 取模:均匀分布,扩容需要迁移数据
- 范围分片:范围查询友好,可能导致热点
- 一致性哈希:扩容只需迁移部分数据
分库分表带来的问题
- 分布式事务:Seata、TCC、Saga、消息最终一致性
- 跨库 JOIN:冗余字段、应用层组装、全局表
- 分布式 ID:UUID、雪花算法、数据库号段、Redis 自增
- 排序分页:各分片查询后合并排序(查询放大问题)
- 扩容:一致性哈希、翻倍扩容(减少数据迁移)
第十章 分布式系统原理
10.1 CAP 定理
- Consistency(一致性):所有节点同一时间数据一致
- Availability(可用性):每个请求都能得到非错响应
- Partition tolerance(分区容忍性):网络分区时系统仍能运行
- 三者只能满足其二,分布式系统中 P 必须保证,所以是 CP 或 AP 的取舍
10.2 BASE 理论
- Basically Available(基本可用):允许响应时间延长或功能降级
- Soft state(软状态):允许中间状态存在
- Eventually consistent(最终一致性):经过一段时间后达到一致
10.3 分布式事务
2PC(两阶段提交)
- Prepare 阶段:协调者询问参与者是否可以提交
- Commit/Abort 阶段:全部同意则提交,否则回滚
- 问题:同步阻塞、单点故障、数据不一致(协调者挂在 Commit 阶段)
3PC(三阶段提交)
- CanCommit → PreCommit → DoCommit
- 引入超时机制,减少阻塞
- 仍无法完全解决数据不一致
TCC(Try-Confirm-Cancel)
- Try:预留资源
- Confirm:确认提交
- Cancel:取消释放资源
- 适合资金、库存等强一致性场景
- 实现复杂,需要考虑幂等、空回滚、悬挂等问题
Saga 模式
- 长事务拆分为多个本地事务,每个有对应的补偿事务
- 向前恢复(重试)或向后恢复(补偿)
- 编排式(中心协调器)vs 协同式(事件驱动)
消息最终一致性
- 本地事务 + 消息表 + 定时扫描
- 或使用 RocketMQ 事务消息
10.4 分布式 ID
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 简单,无需中心化 | 无序,不适合索引,占用空间大 |
| 数据库自增 | 简单有序 | 性能瓶颈,单点故障 |
| 号段模式(Leaf) | 减少 DB 访问 | 号段耗尽时可能阻塞 |
| 雪花算法 | 有序、高性能 | 时钟回拨问题 |
| Redis 自增 | 有序、高性能 | 依赖 Redis 可用性 |
雪花算法(Snowflake)
- 64 位 = 1(符号位)+ 41(时间戳)+ 10(机器ID)+ 12(序列号)
- 41 位时间戳可用约 69 年
- 10 位机器 ID 支持 1024 个节点
- 12 位序列号每毫秒 4096 个 ID
- 时钟回拨问题:等待、报错、使用扩展位记录回拨次数
10.5 一致性算法
Raft
- Leader 选举:Term + 随机超时 → 发起投票 → 获得多数票成为 Leader
- 日志复制:Leader 接收请求 → 追加日志 → 发送给 Follower → 多数确认后 commit
- 三个角色:Leader / Follower / Candidate
- 比 Paxos 更容易理解和实现
Paxos
- Basic Paxos:Proposer → Acceptor → Learner
- Prepare 阶段:提案编号
- Accept 阶段:提案值
- Multi-Paxos:选出 Leader 减少提案冲突
ZAB(ZooKeeper)
- 类似 Raft,两个阶段:
- 发现(Leader 选举)
- 同步(数据同步)+ 广播(正常工作)
- ZXID:epoch + counter
10.6 服务治理
服务注册与发现
- CP 模型:ZooKeeper(临时节点 + Watch)/ Consul
- AP 模型:Eureka(自我保护机制)/ Nacos(默认 AP,可切换 CP)
限流算法
| 算法 | 原理 | 优缺点 |
|---|---|---|
| 计数器 | 固定窗口计数 | 简单,边界问题 |
| 滑动窗口 | 细分时间窗口 | 更精确 |
| 漏桶 | 固定速率流出 | 削峰填谷,不应对突发 |
| 令牌桶 | 固定速率添加令牌 | 允许突发流量 |
熔断(Circuit Breaker)
- 三种状态:Closed → Open → Half-Open
- Closed:正常调用,统计失败率
- Open:直接失败/降级,不调用远程服务
- Half-Open:尝试少量请求,成功则恢复 Closed
降级策略
- 超时降级、失败次数降级、故障降级
- 返回默认值、缓存数据、降级页面
第十一章 实战难题与解决方案
11.1 内存泄漏排查
问题现象
- 应用运行一段时间后 OOM
- Full GC 频繁但内存回收不了
排查步骤
-
获取堆 dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/dump.hprofjmap -dump:format=b,file=dump.hprof <pid>- 注意:jmap 会触发 Full GC
-
分析 dump 文件:
- MAT(Memory Analyzer Tool)
- VisualVM
- 关注 Dominator Tree、Leak Suspects
-
常见泄漏原因:
- ThreadLocal 未 remove
- 静态集合类持有对象引用
- 数据库连接/IO 流未关闭
- 监听器未注销
- 内部类持有外部类引用
- 缓存无上限(WeakHashMap / Guava Cache 限制大小)
- ClassLoader 泄漏(热部署场景)
-
使用 Arthas 在线诊断:
dashboard:全局概览thread:线程分析heapdump:堆转储vmtool:在线查看对象实例
11.2 CPU 100% 排查
排查步骤
top找到 CPU 最高的 Java 进程 PIDtop -Hp <PID>找到 CPU 最高的线程 TIDprintf '%x\n' <TID>转为十六进制jstack <PID> | grep <十六进制TID> -A 30查看线程堆栈- 分析堆栈:死循环、频繁 GC、锁竞争
常见原因
- 死循环或无限递归
- 正则表达式回溯(灾难性回溯)
- 频繁 Full GC(堆内存不足)
- 序列化/反序列化大对象
- 加密/解密运算
11.3 死锁排查
检测方法
jstack <PID>→ 搜索 "deadlock"- VisualVM / JConsole 检测
- Arthas
thread -b找到阻塞线程
预防
- 按固定顺序获取锁
- 使用 tryLock 设置超时
- 减小锁粒度
- 使用并发工具类替代手动加锁
11.4 线上 Full GC 频繁
可能原因
- 老年代空间不足(大对象直接进入老年代、对象过早晋升)
- 元空间不足(动态生成类过多:反射、CGLIB、Groovy)
- System.gc() 调用
- CMS Concurrent Mode Failure
- 内存泄漏导致老年代持续增长
解决方案
- 调整堆大小和各代比例
- 优化代码减少大对象创建
- 增加
-XX:MetaspaceSize - 添加
-XX:+DisableExplicitGC禁止显式 GC - 切换到 G1/ZGC
11.5 接口超时排查
排查思路
- 网络层:ping/telnet/traceroute 检查网络
- 应用层:
- 日志分析 → 定位慢在哪一步
- 链路追踪(SkyWalking / Zipkin)→ 找到耗时环节
- 数据库慢查询 → EXPLAIN 分析
- 系统层:CPU、内存、磁盘 IO、网络 IO
- 中间件层:Redis 大 key、MQ 堆积、连接池耗尽
常见原因与解决
- 慢 SQL → 优化索引、拆分查询
- 外部调用超时 → 设置合理超时时间、熔断降级
- 锁竞争 → 减小锁粒度、无锁化
- GC 停顿 → JVM 调优
- 连接池耗尽 → 增大连接池、排查连接泄漏
11.6 分布式系统数据一致性问题
问题场景
- 缓存与数据库双写不一致
- 微服务间数据一致性
- 主从同步延迟导致读到旧数据
缓存与数据库双写一致性
Cache Aside Pattern(旁路缓存):
- 读:先读缓存 → 命中返回;未命中 → 读 DB → 写缓存
- 写:先更新 DB → 再删除缓存(而非更新缓存)
- 为什么删除而非更新? 避免并发写导致的不一致
延迟双删:
- 先删缓存 → 更新 DB → 延迟一段时间 → 再删缓存
- 解决:先删缓存后,其他线程读到旧数据并写入缓存的问题
Binlog 订阅(Canal):
- 监听 MySQL Binlog → 异步更新/删除缓存
- 最终一致性保证
11.7 大数据量处理
百万级数据导入导出
- POI SXSSFWorkbook(流式写入,控制内存)
- EasyExcel(阿里开源,基于 SAX 解析)
- 分批读取/写入,控制内存
- 异步导出 + 任务通知
大表数据迁移
- 分批迁移(游标分页、范围分页)
- 双写方案(同时写新旧表,对比后切换)
- Binlog 同步(Canal / DTS)
海量数据去重
- BitMap / Bloom Filter(布隆过滤器)
- Redis HyperLogLog(基数估算)
- 分治:先 Hash 分桶再各桶去重
11.8 高并发秒杀系统设计
核心要点
- 前端:页面静态化 CDN、按钮防重复点击、答题验证
- 接入层:Nginx 限流、本地缓存热点数据
- 应用层:
- Redis 预减库存(原子操作 DECR)
- 库存不足直接返回,减少 DB 压力
- 异步下单(MQ 削峰)
- 数据层:
- 数据库乐观锁更新库存
WHERE stock > 0 - 分段锁(库存拆分到多行)
- 数据库乐观锁更新库存
防超卖
- Redis 原子操作
DECR+ 判断 >= 0 - Lua 脚本保证原子性
- 数据库层面
UPDATE SET stock = stock - 1 WHERE stock > 0
11.9 分布式定时任务
问题
- 多实例部署时,定时任务重复执行
解决方案
- 分布式锁:执行前获取 Redis/ZooKeeper 锁
- XXL-JOB:中心化调度,支持分片广播
- Elastic-Job:去中心化,基于 ZooKeeper
- 数据库行锁:
SELECT FOR UPDATE抢占任务
11.10 日志排查与链路追踪
日志最佳实践
- MDC(Mapped Diagnostic Context)传递 traceId
- 异步线程需要传递 MDC 上下文
- 日志级别:ERROR(需要关注的错误)、WARN(可恢复异常)、INFO(重要业务信息)、DEBUG(调试信息)
链路追踪方案
- SkyWalking:字节码增强,无侵入
- Zipkin/Jaeger:基于 OpenTracing/OpenTelemetry
- 核心概念:TraceId(链路ID)→ SpanId(节点ID)→ ParentSpanId(父节点ID)
第十二章 性能优化深度指南
12.1 JVM 层优化
12.1.1 GC 调优
目标确定:
- 吞吐量优先:选择 Parallel GC,关注
-XX:GCTimeRatio - 延迟优先:选择 G1/ZGC,关注
-XX:MaxGCPauseMillis - 内存优先:选择 ZGC/Shenandoah
调优步骤:
- 收集 GC 日志:
-Xlog:gc*:file=gc.log:time,uptime,level,tags - 分析 GC 日志:GCViewer、GCEasy
- 关注指标:GC 频率、GC 耗时、吞吐量、堆使用率
- 调整参数:堆大小、新生代比例、晋升阈值
- 压测验证
常见调优:
- Minor GC 频繁:增大新生代 / 增大 Eden 比例
- Full GC 频繁:增大老年代 / 检查内存泄漏 / 调整晋升阈值
- GC 停顿长:切换到低延迟收集器 / 减小堆(减少标记时间)
G1 调优关键参数:
-XX:G1HeapRegionSize:Region 大小(1-32MB,2的幂)-XX:G1NewSizePercent:新生代最小占比(默认5%)-XX:G1MaxNewSizePercent:新生代最大占比(默认60%)-XX:InitiatingHeapOccupancyPercent:触发混合 GC 的老年代占比(默认45%)-XX:G1MixedGCLiveThresholdPercent:老年代 Region 存活率阈值(默认85%)
12.1.2 内存优化
- 避免创建不必要的对象:复用对象、对象池
- 使用基本类型替代包装类
- String 拼接用 StringBuilder
- 及时释放引用(= null 在方法内效果不大,但在大数组/集合中有用)
- 合理使用弱引用/软引用做缓存
- 注意 Stream/Lambda 中的装箱/拆箱(用 IntStream 等原始类型流)
12.1.3 JIT 优化配合
- 方法保持较小有利于内联:
-XX:MaxInlineSize(默认35字节码)、-XX:FreqInlineSize(频繁调用的方法) - 避免大方法导致 JIT 无法编译
- 使用 final 修饰有助于方法内联和常量折叠
12.2 代码层优化
12.2.1 集合优化
- 初始化集合时指定大小:
new ArrayList<>(expectedSize),new HashMap<>(expectedSize / 0.75 + 1) - 遍历 Map 用
entrySet()而非先keySet()再get() Arrays.asList()返回的是固定大小的 List,不能增删Collections.unmodifiableXxx()创建不可变集合- Java 9+
List.of()/Map.of()创建不可变集合
12.2.2 异常优化
- 异常创建填充堆栈是昂贵操作(
fillInStackTrace()) - 不要用异常做流程控制
- 高性能场景可考虑预创建异常对象并覆写
fillInStackTrace()返回 this
12.2.3 锁优化
- 减小锁粒度:ConcurrentHashMap 分段锁思想
- 减少锁持有时间:锁内只放必要代码
- 读写分离:ReadWriteLock / StampedLock
- CAS 替代锁:AtomicXxx / LongAdder
- ThreadLocal 避免共享
12.2.4 IO 优化
- 使用 BufferedInputStream/BufferedOutputStream 减少系统调用
- NIO 的 DirectByteBuffer 减少拷贝
- 批量操作减少 IO 次数
- 使用内存映射文件处理大文件
12.3 数据库优化
12.3.1 SQL 优化
EXPLAIN 关键字段:
| 字段 | 重要值 |
|---|---|
| type | system > const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引 |
| rows | 预估扫描行数 |
| Extra | Using index(覆盖索引) / Using filesort(文件排序) / Using temporary(临时表) |
优化方向:
- 避免 SELECT *,只查需要的列
- 避免在 WHERE 中对字段进行函数操作
- 小表驱动大表(EXISTS vs IN 的选择)
- LIMIT 优化:
WHERE id > last_id LIMIT n(游标分页) - 批量插入替代循环单条插入
- 避免深分页:
LIMIT 100000, 10→ 延迟关联
12.3.2 连接池优化
HikariCP 核心参数:
| 参数 | 建议 | 说明 |
|---|---|---|
| maximumPoolSize | CPU核数 × 2 + 磁盘数 | PostgreSQL 建议公式 |
| minimumIdle | 等于 maximumPoolSize | 减少连接创建开销 |
| connectionTimeout | 30000ms | 获取连接超时 |
| idleTimeout | 600000ms | 空闲连接存活时间 |
| maxLifetime | 1800000ms | 连接最大存活时间 |
HikariCP 为什么快?
- FastList 替代 ArrayList(避免 range check / 从尾部开始 remove)
- ConcurrentBag:ThreadLocal + CopyOnWriteArrayList + 直接移交(SynchronousQueue)
- 无锁设计,CAS 操作
12.3.3 慢查询优化实战
- 开启慢查询日志:
slow_query_log = ON,long_query_time = 1 - 分析:
pt-query-digest分析慢查询日志 - EXPLAIN 分析执行计划
- 优化索引
- 重构 SQL(子查询转 JOIN、避免 OR)
- 考虑分库分表
12.4 缓存优化
12.4.1 多级缓存架构
本地缓存(Caffeine/Guava Cache)
↓ 未命中
分布式缓存(Redis)
↓ 未命中
数据库
12.4.2 本地缓存
Caffeine(Java 缓存之王):
- W-TinyLFU 淘汰策略:结合 LRU 和 LFU 的优点
- Window Cache(1%):新数据进入,LRU 策略
- Main Cache(99%):Probation(试用)+ Protected(保护),TinyLFU 策略
- Count-Min Sketch:概率计数器,类似布隆过滤器
- 读缓冲(RingBuffer)+ 写缓冲(MpscGrowableArrayQueue)
- 异步维护缓存策略(避免同步开销)
12.4.3 Redis 优化
-
大 Key 问题:
- 定位:
redis-cli --bigkeys/MEMORY USAGE key - 解决:拆分、压缩、lazy 删除(UNLINK)
- 定位:
-
热 Key 问题:
- 定位:
redis-cli --hotkeys(需要开启 LFU) - 解决:本地缓存、读写分离、key 加随机后缀分散
- 定位:
-
Pipeline:批量发送命令,减少 RTT
-
Lua 脚本:原子执行多个命令
-
合理设置过期时间:避免大量 key 同时过期
12.5 网络层优化
12.5.1 HTTP 优化
- 开启 gzip 压缩
- 使用 HTTP/2 多路复用
- 合理使用 CDN
- 连接池复用(HttpClient/OkHttp 连接池)
12.5.2 序列化优化
| 方案 | 性能 | 大小 | 可读性 | 跨语言 |
|---|---|---|---|---|
| JSON | 较低 | 大 | 好 | 是 |
| Protobuf | 高 | 小 | 差 | 是 |
| Hessian | 中 | 中 | 差 | 是 |
| Kryo | 很高 | 小 | 差 | 否 |
| Java 原生 | 低 | 大 | 差 | 否 |
12.5.3 RPC 优化
- 使用长连接复用
- 合理设置超时时间
- 异步调用(CompletableFuture)
- 选择高性能序列化框架
12.6 系统层优化
12.6.1 Linux 内核参数
net.core.somaxconn:SYN 队列长度net.ipv4.tcp_max_syn_backlog:SYN 队列最大长度net.ipv4.tcp_tw_reuse:TIME_WAIT 端口复用vm.swappiness:控制 swap 使用倾向(建议设为 1-10)ulimit -n:最大文件描述符数
12.6.2 JVM 容器化优化
- JDK 8u191+ / JDK 10+ 才能正确识别容器资源限制
-XX:+UseContainerSupport:识别容器内存和 CPU 限制-XX:MaxRAMPercentage=75.0:堆内存占容器内存的比例- 注意:容器内存限制包含堆外内存(Direct Memory、Metaspace、线程栈等)
12.7 性能监控体系
12.7.1 指标收集
- JMX:JVM 内置监控接口
- Micrometer:度量门面(类似 SLF4J 之于日志)
- Prometheus + Grafana:时序数据库 + 可视化
- SkyWalking / Pinpoint:APM(应用性能管理)
12.7.2 关键指标
- JVM:堆内存使用率、GC 次数/耗时、线程数
- 系统:CPU 使用率、内存使用率、磁盘 IO、网络 IO
- 应用:QPS/TPS、响应时间(P50/P95/P99)、错误率
- 数据库:连接数、慢查询数、锁等待
- 中间件:Redis 命中率、MQ 堆积量
第十三章 全面横向对比
13.1 HashMap vs ConcurrentHashMap vs Hashtable
| 维度 | HashMap | ConcurrentHashMap | Hashtable | |
|---|---|---|---|---|
| 线程安全 | 否 | 是 | 是 | |
| 锁机制 | 无 | CAS+synchronized(JDK8) | 全表锁(synchronized) | |
| null key/value | 允许 | 不允许 | 不允许 | |
| 性能 | 最高(单线程) | 高(并发) | 低(全表锁) | |
| 迭代器 | fail-fast | 弱一致性 | fail-fast | |
| 底层 | 数组+链表+红黑树 | 同左 | 数组+链表 | |
| 初始容量 | 16 | 16 | 11 | |
| 扩容 | 2倍 | 2倍 | 2倍+1 | |
| hash 计算 | (h=hashCode)^(h>>>16) | spread(h^(h>>>16))&0x7fffffff | hashCode | 直接用 hashCode |
13.2 synchronized vs ReentrantLock vs volatile vs CAS
| 维度 | synchronized | ReentrantLock | volatile | CAS |
|---|---|---|---|---|
| 层面 | JVM | API | JVM | CPU |
| 原子性 | 是 | 是 | 否 | 是(单变量) |
| 可见性 | 是 | 是 | 是 | 是 |
| 有序性 | 是 | 是 | 是 | 否 |
| 可中断 | 否 | 是 | - | - |
| 公平性 | 非公平 | 可选 | - | - |
| 死锁 | 可能 | 可能(tryLock 可避免) | 不会 | 不会 |
| 性能 | 低竞争好(锁升级) | 高竞争好 | 最好 | 低竞争好 |
| 适用场景 | 通用 | 需要高级功能 | 状态标志 | 简单原子操作 |
13.3 ArrayList vs LinkedList vs Vector
| 维度 | ArrayList | LinkedList | Vector |
|---|---|---|---|
| 底层 | 动态数组 | 双向链表 | 动态数组 |
| 随机访问 | O(1) | O(n) | O(1) |
| 头部插入 | O(n) | O(1) | O(n) |
| 尾部插入 | O(1) 均摊 | O(1) | O(1) 均摊 |
| 线程安全 | 否 | 否 | 是(synchronized) |
| 扩容 | 1.5倍 | 无需 | 2倍 |
| 内存占用 | 连续,较少 | 不连续,较多(prev+next) | 连续,较少 |
| 迭代器 | fail-fast | fail-fast | fail-fast |
13.4 TCP vs UDP
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(ACK+重传+排序) | 不可靠 |
| 有序性 | 有序(序列号) | 无序 |
| 传输方式 | 字节流 | 数据报 |
| 流量控制 | 滑动窗口 | 无 |
| 拥塞控制 | 慢启动/拥塞避免/快恢复/快重传 | 无 |
| 头部开销 | 20-60字节 | 8字节 |
| 速度 | 较慢 | 较快 |
| 应用 | HTTP/FTP/SMTP | DNS/DHCP/视频/游戏 |
13.5 BIO vs NIO vs AIO
| 维度 | BIO | NIO | AIO |
|---|---|---|---|
| 模型 | 一个连接一个线程 | 多路复用(一个线程多个连接) | 异步回调 |
| 阻塞 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
| 核心 | Stream | Channel + Buffer + Selector | CompletionHandler |
| 编程复杂度 | 简单 | 复杂 | 较复杂 |
| 吞吐量 | 低 | 高 | 高 |
| 适用场景 | 连接少,短连接 | 连接多,长连接 | 连接多,长连接 |
| 框架 | Tomcat BIO | Netty、Tomcat NIO | 少见 |
13.6 Spring Bean 作用域对比
| 作用域 | 说明 | 线程安全 |
|---|---|---|
| singleton(默认) | 全局唯一实例 | 需要自行保证 |
| prototype | 每次请求创建新实例 | 天然安全 |
| request | 每个 HTTP 请求创建 | Web 环境安全 |
| session | 每个 HTTP Session 创建 | Web 环境安全 |
| globalSession | Portlet 环境 | 已废弃 |
13.7 JDK 动态代理 vs CGLIB vs AspectJ
| 维度 | JDK 动态代理 | CGLIB | AspectJ |
|---|---|---|---|
| 原理 | 反射 + Proxy | ASM 字节码生成子类 | 编译时/加载时织入 |
| 目标限制 | 必须有接口 | 不能 final | 无限制 |
| 性能 | JDK 8+不差 | 生成慢,调用快 | 最快(编译时) |
| 织入时机 | 运行时 | 运行时 | 编译时/加载时 |
| 功能 | 方法级别 | 方法级别 | 字段、构造方法、静态方法等 |
13.8 Kafka vs RocketMQ vs RabbitMQ
| 维度 | Kafka | RocketMQ | RabbitMQ |
|---|---|---|---|
| 语言 | Scala/Java | Java | Erlang |
| 吞吐量 | 百万级/s | 十万级/s | 万级/s |
| 延迟 | ms 级 | ms 级 | μs 级 |
| 可靠性 | 较高 | 高 | 高 |
| 事务消息 | 有 | 原生支持 | 有(不完善) |
| 延迟消息 | 不支持 | 支持 | 支持(插件) |
| 消息回溯 | 支持 | 支持 | 不支持 |
| 消息堆积 | 优秀(磁盘) | 优秀(磁盘) | 一般 |
| 社区 | Apache | Apache | Pivotal |
| 适用场景 | 大数据/日志 | 电商/金融 | 中小型系统 |
13.9 Redis vs Memcached
| 维度 | Redis | Memcached |
|---|---|---|
| 数据结构 | 丰富(5种+) | 仅 KV |
| 持久化 | RDB + AOF | 不支持 |
| 集群 | Redis Cluster | 客户端分片 |
| 线程模型 | 单线程(6.0 多线程IO) | 多线程 |
| 内存管理 | 自主实现 | slab 分配 |
| 数据大小 | 512MB(单value) | 1MB |
| 过期精度 | 秒/毫秒 | 秒 |
| 发布订阅 | 支持 | 不支持 |
13.10 Dubbo vs Spring Cloud vs gRPC
| 维度 | Dubbo | Spring Cloud | gRPC |
|---|---|---|---|
| 通信协议 | dubbo(TCP) | HTTP/REST | HTTP/2 |
| 序列化 | Hessian2 | JSON | Protobuf |
| 注册中心 | ZK/Nacos | Eureka/Nacos/Consul | 无内置 |
| 服务治理 | 丰富 | 丰富 | 基础 |
| 性能 | 高 | 较低 | 高 |
| 跨语言 | 有限 | 天然(REST) | 支持多语言 |
| 生态 | Dubbo 生态 | Netflix/Alibaba | Google 生态 |
13.11 乐观锁 vs 悲观锁
| 维度 | 乐观锁 | 悲观锁 |
|---|---|---|
| 思想 | 假设不会冲突 | 假设会冲突 |
| 实现 | 版本号/CAS | synchronized/Lock/数据库行锁 |
| 适用场景 | 读多写少 | 写多读少 |
| 冲突处理 | 重试 | 等待 |
| 死锁 | 不会 | 可能 |
| 性能 | 低冲突时好 | 高冲突时好 |
| 数据库实现 | WHERE version = ? | SELECT FOR UPDATE |
13.12 进程 vs 线程 vs 协程
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 定义 | 资源分配单位 | CPU 调度单位 | 用户态轻量线程 |
| 切换开销 | 大(上下文切换) | 中 | 小(用户态切换) |
| 共享 | 独立地址空间 | 共享进程资源 | 共享线程栈 |
| 通信 | IPC(管道/消息/共享内存) | 共享内存/锁 | 直接通信 |
| 调度 | 内核 | 内核 | 用户程序 |
| 并行性 | 可并行 | 可并行 | 并发(非并行) |
| Java 支持 | ProcessBuilder | Thread | JDK 21 虚拟线程 |
13.13 JDK 21 虚拟线程 vs 平台线程
| 维度 | 虚拟线程 | 平台线程 |
|---|---|---|
| 映射 | M:N(多对多) | 1:1(一对一) |
| 创建开销 | 极低(~1KB 栈) | 高(~1MB 栈) |
| 数量 | 百万级 | 千级 |
| 调度 | JVM ForkJoinPool | OS 内核 |
| 适用 | IO 密集型 | CPU 密集型 |
| synchronized | 可能 pin 住载体线程 | 正常 |
| ThreadLocal | 不推荐(数量大开销大) | 正常 |
13.14 Spring 事务传播行为对比总结
| 传播行为 | 外部有事务 | 外部无事务 | 使用场景 |
|---|---|---|---|
| REQUIRED | 加入 | 新建 | 默认,大部分场景 |
| SUPPORTS | 加入 | 非事务 | 查询方法 |
| MANDATORY | 加入 | 异常 | 必须在事务中执行 |
| REQUIRES_NEW | 挂起,新建 | 新建 | 独立事务(日志记录) |
| NOT_SUPPORTED | 挂起,非事务 | 非事务 | 不需要事务 |
| NEVER | 异常 | 非事务 | 确保非事务环境 |
| NESTED | 嵌套(保存点) | 新建 | 子事务可独立回滚 |
13.15 各种设计模式在框架中的应用对比
| 设计模式 | 框架应用 |
|---|---|
| 工厂方法 | Spring BeanFactory、LoggerFactory |
| 抽象工厂 | Spring FactoryBean |
| 单例 | Spring Bean 默认作用域、Runtime |
| 代理 | Spring AOP(JDK/CGLIB)、MyBatis Mapper |
| 策略 | Spring Resource、Comparator |
| 模板方法 | JdbcTemplate、RestTemplate、AbstractApplicationContext.refresh() |
| 观察者 | Spring ApplicationEvent |
| 适配器 | Spring MVC HandlerAdapter |
| 装饰器 | Java IO Stream、Spring HttpServletRequestWrapper |
| 责任链 | Spring Security FilterChain、Netty Pipeline、MyBatis 插件 |
| 建造者 | StringBuilder、Stream.Builder、Lombok @Builder |
| 迭代器 | Iterator、Spliterator |
| 组合 | CompositeByteBuf、Spring CompositeComponentDefinition |
第十四章 高难度深底层原理问题
14.1 Java 对象到底是怎么在内存中定位的?
句柄访问
- 堆中划出句柄池
- reference → 句柄 → 到对象实例数据的指针 + 到对象类型数据的指针
- 优点:对象移动(GC 时)只需修改句柄中的指针,reference 不变
- 缺点:两次间接寻址,开销大
直接指针访问(HotSpot 采用)
- reference 直接指向对象地址
- 对象头中包含类型指针(指向方法区类元数据)
- 优点:访问速度快,只需一次间接寻址
- 缺点:对象移动时需要更新所有 reference
14.2 为什么 Java 的 hashCode() 不是内存地址?
Object.hashCode()是 native 方法,由 JVM 生成- HotSpot 默认使用 Marsaglia's xor-shift 随机数生成器(JDK 8+,策略 5)
- hashCode 存储在对象头 Mark Word 中(延迟计算,首次调用时生成)
- 如果对象已偏向锁,调用 hashCode 会撤销偏向锁(因为 Mark Word 空间冲突)
- 可通过
-XX:hashCode=参数选择策略(0-5)
14.3 synchronized 锁升级的细节与争议
偏向锁撤销的代价
- 必须在全局安全点执行
- 需要遍历偏向线程的栈,判断锁对象是否仍在使用
- 撤销代价高昂,如果偏向锁撤销频繁,反而降低性能
- JDK 15 默认禁用偏向锁就是因为撤销代价
锁降级的真相
- 官方说法:锁不可降级
- 实际:在 STW 期间,如果对象没有被任何线程锁定,可以回到无锁状态
- 但这不是运行时的动态降级
批量重偏向和批量撤销
- 批量重偏向(Bulk Rebias):同一个类的对象被多个线程交替锁定时,将偏向锁指向当前线程(epoch 机制)
- 批量撤销(Bulk Revoke):同一个类的对象偏向锁撤销次数超过阈值(40),该类禁用偏向锁
- 参数:
-XX:BiasedLockingBulkRebiasThreshold=20、-XX:BiasedLockingBulkRevokeThreshold=40
14.4 ConcurrentHashMap 的 size() 到底是怎么做到近似精确的?
JDK 8 的方案
- baseCount:无竞争时直接 CAS 更新
- CounterCell[]:竞争激烈时,类似 LongAdder 的分段计数
- size() = baseCount + Σ CounterCell[i].value
- 为什么不完全精确?因为统计时其他线程可能在修改
mappingCount()方法返回 long,避免 int 溢出问题
并发扩容的实现
- transferIndex:从后向前分配迁移任务
- 每个线程领取一段区间(stride,最少 16 个桶)
- 迁移完成后设置 ForwardingNode 标记
- 其他线程发现 ForwardingNode → 去新表操作 / 协助扩容
14.5 ThreadLocal 的 Hash 冲突解决为什么用开放地址法而非链表法?
- 内存效率:开放地址法不需要额外的链表节点
- Cache 友好:连续数组访问比链表跳转更快
- 负载因子:ThreadLocalMap 负载因子是 2/3,较低,冲突概率小
- Key 的特殊性:ThreadLocal 实例数通常很少(一般 < 10),冲突概率极低
- 清理机制:线性探测过程中顺便清理 stale entry(expungeStaleEntry)
14.6 双亲委派模型中 SPI 是如何打破的?底层细节是什么?
问题本质
java.sql.DriverManager由 Bootstrap ClassLoader 加载- 但它需要加载第三方的
java.sql.Driver实现(如 MySQL Driver) - Bootstrap ClassLoader 无法加载 classpath 上的类
解决方案
- 使用 Thread Context ClassLoader(线程上下文类加载器,默认是 AppClassLoader)
ServiceLoader.load(Service.class)内部使用Thread.currentThread().getContextClassLoader()- 实质:父加载器委托子加载器加载(打破了自底向上的委派)
底层流程
DriverManager的 static 块调用ServiceLoader.load(Driver.class)- ServiceLoader 读取
META-INF/services/java.sql.Driver文件 - 通过 Thread Context ClassLoader(AppClassLoader)加载实现类
- 实例化并注册 Driver
14.7 Java 内存模型中的 happens-before 到底意味着什么?
常见误解
- happens-before 不代表时间上的先后顺序
- 它定义的是可见性保证:如果 A happens-before B,那么 A 的结果对 B 可见
与指令重排的关系
- happens-before 允许不影响结果的重排序
- 编译器和处理器只需要保证 happens-before 规则的结果正确
- JMM 是语言级别的内存模型,屏蔽了底层硬件差异
因果一致性
- JMM 保证的是因果一致性(causality),不是顺序一致性
- 顺序一致性(Sequential Consistency)要求所有操作全局有序,代价太高
- happens-before 提供了足够的保证,同时允许优化
14.8 G1 的 SATB 和 CMS 的 Incremental Update 到底有什么本质区别?
CMS - 增量更新
- 记录新增引用:写后屏障记录 A→C 的新引用
- 重新标记时从变化的引用出发再次扫描
- 缺点:重新标记可能很慢(需要扫描大量变化)
G1 - SATB
- 记录被删除的引用:写前屏障记录 B→C 的旧引用
- 保证并发标记开始时的所有引用都不会漏标
- 缺点:可能产生浮动垃圾(标记开始后成为垃圾的对象不会被回收)
- 优点:重新标记阶段只需处理 SATB 队列,速度快
本质区别
- 增量更新是"记录增量":新增的引用需要重新扫描
- SATB 是"保持快照":标记开始时的引用图快照中的对象都认为是存活的
- G1 选择 SATB 的原因:Region 化内存 + 混合回收的需要,SATB 的暂停时间更可控
14.9 为什么 ConcurrentHashMap 的 key 和 value 不能为 null?
- 二义性问题:
map.get(key)返回 null 时,无法区分是 key 不存在还是 value 就是 null - HashMap 可以通过
containsKey()判断,因为在单线程下是安全的 - ConcurrentHashMap 中,
get()和containsKey()之间可能有其他线程修改 - 即使
containsKey()返回 true,get()也可能返回 null(因为中间被删除了) - Doug Lea 的设计哲学:并发容器应该尽量消除这种二义性
14.10 JVM Safe Point(安全点)的实现细节
轮询机制
- HotSpot 使用**轮询(Polling)**方式实现安全点
- 需要进入安全点时,JVM 修改一个特殊的内存页为不可读
- 线程到达安全点时会读取该页(polling page),触发 SIGSEGV 信号
- 信号处理程序将线程挂起
安全点延迟问题
- 可数循环(counted loop)不会放置安全点轮询:
for (int i = 0; i < 10000000; i++)内部不会检查安全点 - 可能导致 GC 等待某个线程很久(TTSP:Time To Safe Point)
- 解决:将 int 改为 long(不可数循环会放置安全点),或拆分循环
JDK 17+ 的改进
- Thread-Local Handshake:不需要全局安全点,可以单独停顿某个线程
- 减少了全局 STW 的需要
14.11 Java Agent 与字节码增强原理
Java Agent
- premain:JVM 启动时通过
-javaagent:参数加载 - agentmain:运行时通过 Attach API 动态加载
InstrumentationAPI:addTransformer()注册 ClassFileTransformer
字节码框架
| 框架 | 层级 | 特点 |
|---|---|---|
| ASM | 指令级 | 最底层,性能最好,使用复杂 |
| Javassist | 源码级 | 使用简单,可以写 Java 代码字符串 |
| Byte Buddy | 高级 API | 类型安全,流式 API,最易用 |
| CGLIB | 代理 | 基于 ASM,主要用于动态代理 |
应用场景
- APM(SkyWalking、Pinpoint):无侵入监控
- 热部署(JRebel)
- Mock 框架(Mockito)
- ORM 延迟加载(Hibernate)
14.12 Unsafe 类到底能做什么?
核心能力
- 内存操作:
allocateMemory、freeMemory、putXxx、getXxx— 直接操作堆外内存 - CAS 操作:
compareAndSwapInt、compareAndSwapLong、compareAndSwapObject - 线程调度:
park、unpark— LockSupport 底层 - 对象操作:
allocateInstance(不调用构造方法创建对象)、objectFieldOffset - 内存屏障:
loadFence、storeFence、fullFence - 类操作:
defineClass、ensureClassInitialized
获取方式
- 不能直接
Unsafe.getUnsafe()(会检查调用者类加载器) - 通过反射获取
theUnsafe字段 - JDK 9+ 提供
VarHandle作为部分替代
14.13 JDK 中的伪共享(False Sharing)问题
什么是伪共享
- CPU 缓存以缓存行(Cache Line,通常 64 字节)为单位
- 两个不相关的变量在同一缓存行中
- 一个线程修改变量 A → 整个缓存行失效 → 另一个线程的变量 B 也需要重新加载
解决方案
- @Contended 注解(JDK 8+):在字段前后添加填充(128 字节),需要
-XX:-RestrictContended - 手动填充:在类中添加无用的 long 字段
- LongAdder 的
Cell类就使用了@Contended
14.14 JVM 如何实现方法调用?invokevirtual 到底做了什么?
五种调用指令
| 指令 | 调用对象 | 分派类型 |
|---|---|---|
| invokestatic | 静态方法 | 静态分派 |
| invokespecial | 构造方法、私有方法、super 调用 | 静态分派 |
| invokevirtual | 虚方法 | 动态分派 |
| invokeinterface | 接口方法 | 动态分派 |
| invokedynamic | Lambda、方法引用 | 动态分派 |
invokevirtual 执行过程
- 找到操作数栈顶的对象的实际类型 C
- 在类型 C 中查找与常量中描述符和名称匹配的方法
- 如果找到,进行访问权限检查,通过则返回方法引用
- 否则,按继承层次从下往上查找父类
- 始终没找到,抛出 AbstractMethodError
vtable 和 itable
- vtable(虚方法表):类的虚方法表,子类方法覆盖父类方法时,子类 vtable 中对应条目指向子类实现
- itable(接口方法表):接口方法表,用于 invokeinterface 的查找
- vtable 在类加载的连接阶段构建
14.15 Java 的 CompletableFuture 底层是如何实现异步编排的?
核心设计
- 基于**完成树(Completion Tree)**的事件驱动模型
- 内部维护一个
Completion栈(单向链表),存储依赖当前 Future 完成的后续操作 - 当 Future 完成时,遍历 Completion 栈,触发后续操作
执行模型
- 同步执行(thenApply):在完成当前 Future 的线程上执行
- 异步执行(thenApplyAsync):提交到 ForkJoinPool.commonPool() 或指定 Executor
- 组合操作:thenCombine(两个都完成)、applyToEither(任一完成)、allOf/anyOf
异常处理
- exceptionally:捕获异常并返回默认值
- handle:处理结果和异常
- whenComplete:消费结果和异常(不转换)
14.16 类加载器的命名空间隔离原理
命名空间
- 每个类加载器有自己的命名空间
- 同一个类被不同类加载器加载,产生的是不同的 Class 对象
ClassA loaded by CL1 ≠ ClassA loaded by CL2
实际影响
- 不同 WebApp 中相同全限定名的类互不干扰(Tomcat WebAppClassLoader)
- OSGi 每个 Bundle 有独立的类加载器,实现模块隔离
- ClassCastException:同一个类被不同类加载器加载后相互转换会失败
14.17 G1 的 Region 为什么选择这个大小?
Region 大小的权衡
- 默认 Region 数量 ≈ 2048 个,大小根据堆大小自动计算
- Region 太大:
- 每次 GC 回收的粒度大,停顿时间长
- Humongous 对象的阈值变高(50% Region),浪费减少
- Region 太小:
- 管理开销大(RSet、Card Table)
- 大对象容易触发 Humongous 分配(连续 Region),可能导致碎片
- 并发标记开销增大
Humongous 对象的影响
- 大于 Region 50% 的对象是 Humongous 对象
- 分配在连续的 Region 中
- 可能导致提前触发 GC
- 回收时机:并发标记阶段或 Full GC
- JDK 8u40+ 在 Young GC 时也可回收 Humongous Region
14.18 为什么 JVM 启动时需要两阶段提交来保证 Redo Log 和 Binlog 一致?
崩溃场景分析
不使用两阶段提交时的问题:
场景1:先写 Redo Log 再写 Binlog
- Redo Log 写成功后崩溃 → 重启后通过 Redo Log 恢复数据
- 但 Binlog 没有该操作 → 从库通过 Binlog 同步后缺少该操作
- 主从数据不一致
场景2:先写 Binlog 再写 Redo Log
- Binlog 写成功后崩溃 → Redo Log 没有,主库数据未变
- 但从库通过 Binlog 同步了该操作 → 从库多出该操作
- 主从数据不一致
两阶段提交如何保证一致?
- Redo Log prepare → Binlog 写入 → Redo Log commit
- 崩溃恢复时:
- Redo Log prepare + Binlog 完整 → 提交
- Redo Log prepare + Binlog 不完整 → 回滚
- Redo Log commit → 已提交,无需处理
14.19 JDK 中 String.intern() 的不同版本行为差异的根本原因
JDK 6
- 字符串常量池在永久代
- intern() 将字符串复制到永久代的常量池中
- 大量 intern() 可能导致永久代 OOM
JDK 7+
- 字符串常量池移到堆中
- intern() 不再复制字符串,而是将堆中字符串的引用放入常量池
- 与之前版本行为不同的原因
经典面试题
String s1 = new String("a") + new String("b"); // 堆中 "ab"
s1.intern(); // JDK 7+:将 s1 的引用放入常量池
String s2 = "ab"; // 从常量池获取,就是 s1 的引用
System.out.println(s1 == s2); // JDK 7+:true(JDK 6:false)
14.20 分代 ZGC(JDK 21)解决了什么问题?
非分代 ZGC 的问题
- 所有对象一视同仁,每次 GC 扫描整个堆
- 短生命周期对象也需要全堆标记
- 浮动垃圾问题更严重(需要更多内存余量)
分代 ZGC 的改进
- 引入年轻代和老年代的概念
- 年轻代 GC 更频繁(minor GC),老年代 GC 较少(major GC)
- 记忆集:记录老年代到年轻代的引用,避免扫描整个老年代
- 写屏障从染色指针上的读屏障变为存储屏障
- 显著降低了内存开销和 GC 频率
性能提升
- 吞吐量提升 10%-20%
- 更低的内存占用
- 更短的暂停时间
- 适合更大的堆(TB 级别)
结语
本文从 JVM 底层到分布式系统,从语言特性到框架原理,从理论到实战,全方位覆盖了 Java 技术栈的核心知识点。每个章节都力求做到:
- 纵向深度:从表象到底层原理,层层递进
- 横向对比:同类技术的全方位比较,帮助理解差异
- 融会贯通:知识点之间的关联和相互影响
- 实战导向:不仅讲原理,更讲如何应用和排查问题
"知其然,知其所以然,知其所以必然。" — 这才是真正掌握技术的标准。
本文持续更新,建议收藏并反复阅读。每一个知识点都值得深入思考和实践验证。