# Java 深度全解(终极版)

2 阅读1小时+

涵盖:底层原理 · 第三方库 · 实战难题 · 性能优化 · 横向对比 · 高难度深层问题 全面、融会贯通、深入浅出、细节到位


目录


第一章 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 对象创建的完整流程

  1. 类加载检查:检查 new 指令的参数能否在常量池中定位到类的符号引用,且该类是否已被加载、解析、初始化
  2. 分配内存
    • 指针碰撞(Bump the Pointer):堆内存规整时(Serial、ParNew 等带压缩的收集器),一个指针作为分界线
    • 空闲列表(Free List):堆内存不规整时(CMS 等基于标记-清除的收集器),维护可用内存块列表
    • 并发安全:CAS + 失败重试 或 TLAB
  3. 内存初始化零值:保证实例字段不赋初值就能使用
  4. 设置对象头:Mark Word + 类型指针(+ 数组长度)
  5. 执行 <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)
  • 目标:最短回收停顿时间
  • 算法:标记-清除
  • 四个阶段
    1. 初始标记(STW):标记 GC Roots 直接关联的对象,速度快
    2. 并发标记:从 GC Roots 开始遍历对象图,与用户线程并发
    3. 重新标记(STW):修正并发标记期间变动的引用(增量更新 Incremental Update)
    4. 并发清除:清理已标记的对象,与用户线程并发
  • 缺点
    • 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
  • 四个阶段
    1. 初始标记(STW):标记 GC Roots 直接关联对象,修改 TAMS 指针
    2. 并发标记:可达性分析,使用 SATB(Snapshot-At-The-Beginning)
    3. 最终标记(STW):处理 SATB 缓冲区中的引用变化
    4. 筛选回收(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):满足两个条件同时成立:
    1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
    2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
  • 解决方案
    • 增量更新(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)

  1. 通过全限定名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在堆中生成 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:SurvivorRatioEden:Survivor 比值
-XX:MaxTenuringThreshold对象晋升老年代的年龄阈值
-XX:PretenureSizeThreshold直接进入老年代的对象大小阈值
-XX:+UseCompressedOops开启压缩指针
-XX:+PrintGCDetails打印 GC 详细日志
-XX:+HeapDumpOnOutOfMemoryErrorOOM 时自动 dump

第二章 Java 语言核心底层

2.1 基本数据类型与包装类

2.1.1 八大基本类型

类型大小(byte)范围默认值包装类
byte1-128~1270Byte
short2-32768~327670Short
int4-2^31~2^31-10Integer
long8-2^63~2^63-10LLong
float4IEEE 7540.0fFloat
double8IEEE 7540.0dDouble
char20~65535'\u0000'Character
boolean~1true/falsefalseBoolean

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 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

维度StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全安全(不可变)不安全安全(synchronized)
性能拼接慢最快比StringBuilder慢
底层char[]/byte[]char[](可扩容)char[](可扩容)
扩容策略-(旧容量 << 1) + 2同 StringBuilder

2.3 面向对象深度

2.3.1 多态的底层实现

  • 方法表(vtable):每个类在方法区有一个虚方法表,存储各虚方法的实际入口地址
  • 子类的方法表包含父类方法表的副本,覆写的方法指向子类的实现
  • invokevirtual 指令执行时,通过对象的实际类型找到方法表,定位方法入口
  • 内联缓存(Inline Cache):JIT 优化,缓存方法调用的目标地址
    • 单态内联缓存:只缓存一种类型 → 直接调用
    • 多态内联缓存:缓存多种类型 → 条件判断
    • 超多态:退化为虚方法表查找

2.3.2 接口的默认方法(Java 8)

  • 解决接口演化问题(向后兼容)
  • 菱形继承冲突解决规则
    1. 类中声明的方法优先级最高
    2. 子接口优先级高于父接口
    3. 无法确定时必须显式覆写

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:动态生成字节码的委派实现,超过阈值后切换,性能更好

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 原则

  1. 程序顺序规则:同一线程中,前面的操作 happens-before 后面的操作
  2. 监视器锁规则:unlock happens-before 后续对同一把锁的 lock
  3. volatile 规则:volatile 写 happens-before 后续对同一 volatile 的读
  4. 线程启动规则:Thread.start() happens-before 该线程中的任何操作
  5. 线程终止规则:线程中所有操作 happens-before 其他线程检测到该线程终止
  6. 中断规则:线程 interrupt() 调用 happens-before 被中断线程检测到中断
  7. 终结器规则:构造函数完成 happens-before finalize() 开始
  8. 传递性: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 前缀指令效果:
    1. 将当前处理器缓存行写回主内存
    2. 使其他 CPU 中缓存了该内存地址的数据无效(MESI 协议嗅探)

volatile 的语义

  • 可见性:一个线程修改 volatile 变量后,其他线程立即可见
  • 有序性:禁止指令重排(通过插入内存屏障)
  • 不保证原子性volatile int count; count++ 不是原子操作

volatile 的内存屏障插入策略

  • volatile 写前插入 StoreStore 屏障
  • volatile 写后插入 StoreLoad 屏障
  • volatile 读后插入 LoadLoad 屏障
  • volatile 读后插入 LoadStore 屏障

典型应用

  • DCL 单例模式中防止指令重排
  • 状态标志位
  • 一写多读场景

3.3 synchronized 深度

3.3.1 底层实现

  • 代码块:编译后在同步块前后插入 monitorentermonitorexit 指令
  • 方法:方法标志位设置 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

轻量级锁

  1. 在当前线程栈帧中创建 Lock Record
  2. 将 Mark Word 复制到 Lock Record(Displaced Mark Word)
  3. CAS 尝试将 Mark Word 更新为指向 Lock Record 的指针
  4. 成功则获取锁;失败则自旋重试
  5. 自旋超过阈值或有第三个线程竞争,升级为重量级锁

自适应自旋

  • 根据上次自旋是否成功动态调整自旋次数
  • 成功过 → 增加自旋次数;从未成功 → 跳过自旋

重量级锁

  • 通过 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 独占模式获取锁流程

  1. acquire(1)tryAcquire(1)(子类实现)
  2. 获取失败 → addWaiter(Node.EXCLUSIVE) 加入 CLH 队列尾部(CAS)
  3. 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

维度synchronizedReentrantLock
实现层面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 执行流程

  1. 当前线程数 < corePoolSize → 创建核心线程执行
  2. 当前线程数 >= corePoolSize → 任务入队
  3. 队列满 && 线程数 < maximumPoolSize → 创建非核心线程执行
  4. 队列满 && 线程数 >= 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 无界队列,可能导致 OOM
  • newCachedThreadPool:maximumPoolSize 为 Integer.MAX_VALUE,可能创建大量线程导致 OOM
  • newScheduledThreadPool:同上问题
  • 应该使用 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

维度CountDownLatchCyclicBarrierSemaphore
作用等待 N 个事件完成N 个线程互相等待到达屏障控制并发访问数量
是否可重用不可重用可重用(reset)不涉及
底层AQS 共享模式ReentrantLock + ConditionAQS 共享模式
计数方向递减(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 的三个问题

  1. ABA 问题:值从 A→B→A,CAS 认为没变化
    • 解决:AtomicStampedReference(版本号)/ AtomicMarkableReference(标记位)
  2. 自旋开销:长时间 CAS 失败导致 CPU 空转
    • 解决:LongAdder 分段思想、退避策略
  3. 只能保证一个变量的原子操作
    • 解决: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 流程

  1. 计算 key 的 hash
  2. 如果 table 为 null 或 length == 0 → resize() 初始化
  3. 计算桶位 (n-1) & hash
  4. 桶位为空 → 直接放入新节点
  5. 桶位不为空:
    • 首节点 key 相同 → 覆盖
    • 首节点是 TreeNode → 红黑树插入
    • 链表遍历:找到相同 key 则覆盖;否则尾插法插入,判断是否需要树化
  6. 判断 ++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 监控多个 fdselect 调用阻塞
信号驱动 IO数据就绪时内核发信号拷贝数据时阻塞
异步 IO(AIO)全程不阻塞,完成时回调不阻塞

5.1.2 select vs poll vs epoll

维度selectpollepoll
数据结构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 的数据拷贝

  1. 磁盘 → 内核缓冲区(DMA 拷贝)
  2. 内核缓冲区 → 用户缓冲区(CPU 拷贝)
  3. 用户缓冲区 → Socket 缓冲区(CPU 拷贝)
  4. Socket 缓冲区 → 网卡(DMA 拷贝)
  • 4 次拷贝,4 次上下文切换

sendfile(零拷贝)

  1. 磁盘 → 内核缓冲区(DMA 拷贝)
  2. 内核缓冲区 → Socket 缓冲区(CPU 拷贝 / DMA gather)
  3. Socket 缓冲区 → 网卡(DMA 拷贝)
  • 最少 2 次拷贝(DMA gather 时),2 次上下文切换

mmap(内存映射)

  1. 磁盘 → 内核缓冲区(DMA 拷贝)
  2. 用户空间映射到内核缓冲区(无拷贝)
  3. 内核缓冲区 → Socket 缓冲区(CPU 拷贝)
  4. Socket 缓冲区 → 网卡(DMA 拷贝)
  • 3 次拷贝,4 次上下文切换

Java 中的零拷贝

  • FileChannel.transferTo() → sendfile
  • MappedByteBuffer → 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 核心流程

  1. 资源定位:通过 ResourceLoader 定位 Bean 定义资源(XML/注解/JavaConfig)
  2. Bean 定义加载:BeanDefinitionReader 读取并解析为 BeanDefinition
  3. Bean 定义注册:注册到 BeanDefinitionRegistry(底层 ConcurrentHashMap)
  4. Bean 实例化与初始化
    • 实例化(Instantiation):反射创建对象 / FactoryMethod
    • 属性注入(Populate):依赖注入
    • 初始化(Initialization):Aware 回调 → BeanPostProcessor.postProcessBeforeInitialization → InitializingBean.afterPropertiesSet / init-method → BeanPostProcessor.postProcessAfterInitialization

6.1.2 Bean 的完整生命周期

  1. 实例化(构造方法或工厂方法)
  2. 属性注入(setter、@Autowired 等)
  3. BeanNameAware.setBeanName()
  4. BeanFactoryAware.setBeanFactory()
  5. ApplicationContextAware.setApplicationContext()
  6. BeanPostProcessor.postProcessBeforeInitialization()
  7. @PostConstruct
  8. InitializingBean.afterPropertiesSet()
  9. 自定义 init-method
  10. BeanPostProcessor.postProcessAfterInitialization()(AOP 代理在此创建)
  11. Bean 就绪,使用中
  12. @PreDestroy
  13. DisposableBean.destroy()
  14. 自定义 destroy-method

6.1.3 循环依赖解决(三级缓存)

  • 一级缓存 singletonObjects:完全初始化的 Bean(ConcurrentHashMap)
  • 二级缓存 earlySingletonObjects:提前暴露的 Bean(未完成属性注入,HashMap)
  • 三级缓存 singletonFactories:ObjectFactory(lambda,延迟创建代理,HashMap)

解决流程(A 依赖 B,B 依赖 A):

  1. 创建 A → 实例化 → 将 ObjectFactory(A) 放入三级缓存
  2. A 属性注入 → 发现需要 B → 创建 B
  3. 创建 B → 实例化 → 将 ObjectFactory(B) 放入三级缓存
  4. B 属性注入 → 发现需要 A → 从三级缓存获取 ObjectFactory(A) → 调用 getObject() → 得到 A 的早期引用(可能是代理)→ 放入二级缓存,删除三级缓存
  5. B 完成初始化 → 放入一级缓存
  6. A 拿到 B → 完成初始化 → 放入一级缓存

为什么需要三级缓存而不是两级?

  • 如果没有 AOP,两级缓存就够了
  • 三级缓存的 ObjectFactory 负责在需要时创建代理对象
  • 确保无论何时获取 Bean,得到的都是正确的代理对象
  • 延迟代理创建,如果没有循环依赖,代理在初始化后创建

哪些情况无法解决循环依赖?

  • 构造器注入:实例化阶段就需要依赖,此时还没来得及放入缓存
  • prototype 作用域:不缓存 Bean
  • @Async:因为后置处理器创建了新的代理对象
  • 解决方案:@Lazy、setter 注入、@DependsOn

6.1.4 BeanFactory vs ApplicationContext

维度BeanFactoryApplicationContext
Bean 加载懒加载预加载(启动时实例化单例)
功能基础 IoCIoC + AOP + 事件 + 国际化 + 资源访问
实现DefaultListableBeanFactoryClassPathXmlApplicationContext / 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 流程

  1. DispatcherServlet 接收请求
  2. HandlerMapping 查找 Handler(Controller 方法)
  3. HandlerAdapter 适配并执行 Handler
  4. Handler 返回 ModelAndView
  5. ViewResolver 解析视图
  6. 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:组件扫描

自动配置流程

  1. @EnableAutoConfiguration@Import(AutoConfigurationImportSelector.class)
  2. AutoConfigurationImportSelector.selectImports()
  3. 通过 SpringFactoriesLoader.loadFactoryNames() 读取 META-INF/spring.factories
  4. 加载所有 EnableAutoConfiguration 对应的配置类
  5. 通过 @Conditional 系列注解条件过滤(@ConditionalOnClass@ConditionalOnMissingBean 等)

@Conditional 系列

注解条件
@ConditionalOnClassclasspath 中存在指定类
@ConditionalOnMissingClassclasspath 中不存在指定类
@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 失效场景

  1. 方法非 public:Spring AOP 默认只代理 public 方法
  2. 自调用:同一类中方法调用不经过代理(this.method() 不走 AOP)
  3. 异常被捕获:catch 吞掉异常,事务无法感知
  4. rollbackFor 不匹配:默认只对 RuntimeException 和 Error 回滚
  5. 传播行为不当:SUPPORTS 等可能导致无事务
  6. 多线程:事务基于 ThreadLocal,新线程不在事务中
  7. 非 Spring 管理的 Bean:new 出来的对象没有代理
  8. 数据库引擎不支持:如 MyISAM 不支持事务

6.5.3 NESTED vs REQUIRES_NEW

维度NESTEDREQUIRES_NEW
本质保存点(savepoint)独立事务
外部回滚一起回滚不影响(已提交)
内部回滚回滚到保存点,外部可继续不影响外部
数据库要求需要支持 savepoint任何支持事务的数据库

6.6 Spring Security

核心架构

  • SecurityFilterChain:一组 Filter 组成的安全过滤链
  • 核心 Filter
    • UsernamePasswordAuthenticationFilter:表单登录
    • BasicAuthenticationFilter:HTTP Basic 认证
    • FilterSecurityInterceptor:授权决策
    • ExceptionTranslationFilter:异常处理

认证流程

  1. AuthenticationFilter 拦截请求
  2. 创建 Authentication(UsernamePasswordAuthenticationToken)
  3. AuthenticationManager.authenticate() → 委托给 AuthenticationProvider
  4. AuthenticationProvider 调用 UserDetailsService.loadUserByUsername()
  5. 验证密码(PasswordEncoder)
  6. 认证成功 → 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 执行流程

  1. SqlSession 调用 Executor
  2. Executor 调用 StatementHandler
  3. StatementHandler → ParameterHandler 设置参数
  4. StatementHandler 执行 SQL
  5. ResultSetHandler 处理结果集 → TypeHandler 转换类型
  6. 返回结果

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所有 keyLRU 淘汰
allkeys-lfu所有 keyLFU 淘汰
allkeys-random所有 key随机淘汰
volatile-lru有过期时间的 keyLRU 淘汰
volatile-lfu有过期时间的 keyLFU 淘汰
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 单线程为什么快?

  1. 纯内存操作
  2. 单线程避免上下文切换和锁竞争
  3. IO 多路复用(epoll)
  4. 高效的数据结构
  5. 注意: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 保持同步的副本集合
  • 消费者组:一个分区只能被组内一个消费者消费

高性能原因

  1. 顺序写磁盘(追加写 log 文件)
  2. 零拷贝(sendfile)
  3. 分区并行
  4. 批量发送和压缩
  5. Page Cache 利用操作系统缓存
  6. 稀疏索引(.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 的区别

维度KafkaRocketMQ
设计目标高吞吐可靠性+功能丰富
事务消息有(但不同)原生支持
延迟消息不支持(需自实现)原生支持
消息回溯按 offset按时间戳
刷盘异步同步/异步
注册中心ZooKeeper/KRaftNameServer
消息查询不支持支持(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 服务调用流程

  1. Consumer 从 Registry 获取 Provider 列表
  2. 通过负载均衡选择 Provider
  3. 通过 Netty 发送序列化后的请求
  4. 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 写入流程

  1. 写入请求到协调节点
  2. 路由到对应分片(_id hash % 分片数)
  3. 写入 Translog(预写日志)
  4. 写入 Index Buffer
  5. Refresh:Index Buffer → Segment(内存中,可搜索)
  6. 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)

  1. Redo Log prepare
  2. 写入 Binlog
  3. 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

索引失效场景

  1. 对索引列使用函数/运算
  2. 隐式类型转换
  3. LIKE 以 % 开头
  4. OR 条件中有非索引列
  5. 不满足最左前缀原则
  6. 优化器判断全表扫描更快

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 LogInnoDB 引擎层物理修改(数据页)崩溃恢复(持久性)
Undo LogInnoDB 引擎层逻辑回滚操作事务回滚 + MVCC
BinlogMySQL Server 层SQL/行变更主从复制 + 数据恢复
Slow Query LogMySQL Server 层慢 SQL性能分析

9.2 分库分表

拆分策略

  • 垂直分库:按业务拆分(用户库、订单库、商品库)
  • 垂直分表:按字段拆分(常用字段和不常用字段分开)
  • 水平分库:相同结构,按规则分散数据到不同库
  • 水平分表:相同结构,按规则分散数据到不同表

分片算法

  • Hash 取模:均匀分布,扩容需要迁移数据
  • 范围分片:范围查询友好,可能导致热点
  • 一致性哈希:扩容只需迁移部分数据

分库分表带来的问题

  1. 分布式事务:Seata、TCC、Saga、消息最终一致性
  2. 跨库 JOIN:冗余字段、应用层组装、全局表
  3. 分布式 ID:UUID、雪花算法、数据库号段、Redis 自增
  4. 排序分页:各分片查询后合并排序(查询放大问题)
  5. 扩容:一致性哈希、翻倍扩容(减少数据迁移)

第十章 分布式系统原理

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 频繁但内存回收不了

排查步骤

  1. 获取堆 dump

    • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/dump.hprof
    • jmap -dump:format=b,file=dump.hprof <pid>
    • 注意:jmap 会触发 Full GC
  2. 分析 dump 文件

    • MAT(Memory Analyzer Tool)
    • VisualVM
    • 关注 Dominator Tree、Leak Suspects
  3. 常见泄漏原因

    • ThreadLocal 未 remove
    • 静态集合类持有对象引用
    • 数据库连接/IO 流未关闭
    • 监听器未注销
    • 内部类持有外部类引用
    • 缓存无上限(WeakHashMap / Guava Cache 限制大小)
    • ClassLoader 泄漏(热部署场景)
  4. 使用 Arthas 在线诊断

    • dashboard:全局概览
    • thread:线程分析
    • heapdump:堆转储
    • vmtool:在线查看对象实例

11.2 CPU 100% 排查

排查步骤

  1. top 找到 CPU 最高的 Java 进程 PID
  2. top -Hp <PID> 找到 CPU 最高的线程 TID
  3. printf '%x\n' <TID> 转为十六进制
  4. jstack <PID> | grep <十六进制TID> -A 30 查看线程堆栈
  5. 分析堆栈:死循环、频繁 GC、锁竞争

常见原因

  • 死循环或无限递归
  • 正则表达式回溯(灾难性回溯)
  • 频繁 Full GC(堆内存不足)
  • 序列化/反序列化大对象
  • 加密/解密运算

11.3 死锁排查

检测方法

  • jstack <PID> → 搜索 "deadlock"
  • VisualVM / JConsole 检测
  • Arthas thread -b 找到阻塞线程

预防

  • 按固定顺序获取锁
  • 使用 tryLock 设置超时
  • 减小锁粒度
  • 使用并发工具类替代手动加锁

11.4 线上 Full GC 频繁

可能原因

  1. 老年代空间不足(大对象直接进入老年代、对象过早晋升)
  2. 元空间不足(动态生成类过多:反射、CGLIB、Groovy)
  3. System.gc() 调用
  4. CMS Concurrent Mode Failure
  5. 内存泄漏导致老年代持续增长

解决方案

  1. 调整堆大小和各代比例
  2. 优化代码减少大对象创建
  3. 增加 -XX:MetaspaceSize
  4. 添加 -XX:+DisableExplicitGC 禁止显式 GC
  5. 切换到 G1/ZGC

11.5 接口超时排查

排查思路

  1. 网络层:ping/telnet/traceroute 检查网络
  2. 应用层
    • 日志分析 → 定位慢在哪一步
    • 链路追踪(SkyWalking / Zipkin)→ 找到耗时环节
    • 数据库慢查询 → EXPLAIN 分析
  3. 系统层:CPU、内存、磁盘 IO、网络 IO
  4. 中间件层: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 高并发秒杀系统设计

核心要点

  1. 前端:页面静态化 CDN、按钮防重复点击、答题验证
  2. 接入层:Nginx 限流、本地缓存热点数据
  3. 应用层
    • Redis 预减库存(原子操作 DECR)
    • 库存不足直接返回,减少 DB 压力
    • 异步下单(MQ 削峰)
  4. 数据层
    • 数据库乐观锁更新库存 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

调优步骤

  1. 收集 GC 日志:-Xlog:gc*:file=gc.log:time,uptime,level,tags
  2. 分析 GC 日志:GCViewer、GCEasy
  3. 关注指标:GC 频率、GC 耗时、吞吐量、堆使用率
  4. 调整参数:堆大小、新生代比例、晋升阈值
  5. 压测验证

常见调优

  • 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 关键字段

字段重要值
typesystem > const > eq_ref > ref > range > index > ALL
key实际使用的索引
rows预估扫描行数
ExtraUsing index(覆盖索引) / Using filesort(文件排序) / Using temporary(临时表)

优化方向

  • 避免 SELECT *,只查需要的列
  • 避免在 WHERE 中对字段进行函数操作
  • 小表驱动大表(EXISTS vs IN 的选择)
  • LIMIT 优化:WHERE id > last_id LIMIT n(游标分页)
  • 批量插入替代循环单条插入
  • 避免深分页:LIMIT 100000, 10 → 延迟关联

12.3.2 连接池优化

HikariCP 核心参数

参数建议说明
maximumPoolSizeCPU核数 × 2 + 磁盘数PostgreSQL 建议公式
minimumIdle等于 maximumPoolSize减少连接创建开销
connectionTimeout30000ms获取连接超时
idleTimeout600000ms空闲连接存活时间
maxLifetime1800000ms连接最大存活时间

HikariCP 为什么快?

  • FastList 替代 ArrayList(避免 range check / 从尾部开始 remove)
  • ConcurrentBag:ThreadLocal + CopyOnWriteArrayList + 直接移交(SynchronousQueue)
  • 无锁设计,CAS 操作

12.3.3 慢查询优化实战

  1. 开启慢查询日志:slow_query_log = ON, long_query_time = 1
  2. 分析:pt-query-digest 分析慢查询日志
  3. EXPLAIN 分析执行计划
  4. 优化索引
  5. 重构 SQL(子查询转 JOIN、避免 OR)
  6. 考虑分库分表

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

维度HashMapConcurrentHashMapHashtable
线程安全
锁机制CAS+synchronized(JDK8)全表锁(synchronized)
null key/value允许不允许不允许
性能最高(单线程)高(并发)低(全表锁)
迭代器fail-fast弱一致性fail-fast
底层数组+链表+红黑树同左数组+链表
初始容量161611
扩容2倍2倍2倍+1
hash 计算(h=hashCode)^(h>>>16)spread(h^(h>>>16))&0x7fffffffhashCode直接用 hashCode

13.2 synchronized vs ReentrantLock vs volatile vs CAS

维度synchronizedReentrantLockvolatileCAS
层面JVMAPIJVMCPU
原子性是(单变量)
可见性
有序性
可中断--
公平性非公平可选--
死锁可能可能(tryLock 可避免)不会不会
性能低竞争好(锁升级)高竞争好最好低竞争好
适用场景通用需要高级功能状态标志简单原子操作

13.3 ArrayList vs LinkedList vs Vector

维度ArrayListLinkedListVector
底层动态数组双向链表动态数组
随机访问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-fastfail-fastfail-fast

13.4 TCP vs UDP

维度TCPUDP
连接面向连接(三次握手)无连接
可靠性可靠(ACK+重传+排序)不可靠
有序性有序(序列号)无序
传输方式字节流数据报
流量控制滑动窗口
拥塞控制慢启动/拥塞避免/快恢复/快重传
头部开销20-60字节8字节
速度较慢较快
应用HTTP/FTP/SMTPDNS/DHCP/视频/游戏

13.5 BIO vs NIO vs AIO

维度BIONIOAIO
模型一个连接一个线程多路复用(一个线程多个连接)异步回调
阻塞同步阻塞同步非阻塞异步非阻塞
核心StreamChannel + Buffer + SelectorCompletionHandler
编程复杂度简单复杂较复杂
吞吐量
适用场景连接少,短连接连接多,长连接连接多,长连接
框架Tomcat BIONetty、Tomcat NIO少见

13.6 Spring Bean 作用域对比

作用域说明线程安全
singleton(默认)全局唯一实例需要自行保证
prototype每次请求创建新实例天然安全
request每个 HTTP 请求创建Web 环境安全
session每个 HTTP Session 创建Web 环境安全
globalSessionPortlet 环境已废弃

13.7 JDK 动态代理 vs CGLIB vs AspectJ

维度JDK 动态代理CGLIBAspectJ
原理反射 + ProxyASM 字节码生成子类编译时/加载时织入
目标限制必须有接口不能 final无限制
性能JDK 8+不差生成慢,调用快最快(编译时)
织入时机运行时运行时编译时/加载时
功能方法级别方法级别字段、构造方法、静态方法等

13.8 Kafka vs RocketMQ vs RabbitMQ

维度KafkaRocketMQRabbitMQ
语言Scala/JavaJavaErlang
吞吐量百万级/s十万级/s万级/s
延迟ms 级ms 级μs 级
可靠性较高
事务消息原生支持有(不完善)
延迟消息不支持支持支持(插件)
消息回溯支持支持不支持
消息堆积优秀(磁盘)优秀(磁盘)一般
社区ApacheApachePivotal
适用场景大数据/日志电商/金融中小型系统

13.9 Redis vs Memcached

维度RedisMemcached
数据结构丰富(5种+)仅 KV
持久化RDB + AOF不支持
集群Redis Cluster客户端分片
线程模型单线程(6.0 多线程IO)多线程
内存管理自主实现slab 分配
数据大小512MB(单value)1MB
过期精度秒/毫秒
发布订阅支持不支持

13.10 Dubbo vs Spring Cloud vs gRPC

维度DubboSpring CloudgRPC
通信协议dubbo(TCP)HTTP/RESTHTTP/2
序列化Hessian2JSONProtobuf
注册中心ZK/NacosEureka/Nacos/Consul无内置
服务治理丰富丰富基础
性能较低
跨语言有限天然(REST)支持多语言
生态Dubbo 生态Netflix/AlibabaGoogle 生态

13.11 乐观锁 vs 悲观锁

维度乐观锁悲观锁
思想假设不会冲突假设会冲突
实现版本号/CASsynchronized/Lock/数据库行锁
适用场景读多写少写多读少
冲突处理重试等待
死锁不会可能
性能低冲突时好高冲突时好
数据库实现WHERE version = ?SELECT FOR UPDATE

13.12 进程 vs 线程 vs 协程

维度进程线程协程
定义资源分配单位CPU 调度单位用户态轻量线程
切换开销大(上下文切换)小(用户态切换)
共享独立地址空间共享进程资源共享线程栈
通信IPC(管道/消息/共享内存)共享内存/锁直接通信
调度内核内核用户程序
并行性可并行可并行并发(非并行)
Java 支持ProcessBuilderThreadJDK 21 虚拟线程

13.13 JDK 21 虚拟线程 vs 平台线程

维度虚拟线程平台线程
映射M:N(多对多)1:1(一对一)
创建开销极低(~1KB 栈)高(~1MB 栈)
数量百万级千级
调度JVM ForkJoinPoolOS 内核
适用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()
  • 实质:父加载器委托子加载器加载(打破了自底向上的委派)

底层流程

  1. DriverManager 的 static 块调用 ServiceLoader.load(Driver.class)
  2. ServiceLoader 读取 META-INF/services/java.sql.Driver 文件
  3. 通过 Thread Context ClassLoader(AppClassLoader)加载实现类
  4. 实例化并注册 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 动态加载
  • Instrumentation API:addTransformer() 注册 ClassFileTransformer

字节码框架

框架层级特点
ASM指令级最底层,性能最好,使用复杂
Javassist源码级使用简单,可以写 Java 代码字符串
Byte Buddy高级 API类型安全,流式 API,最易用
CGLIB代理基于 ASM,主要用于动态代理

应用场景

  • APM(SkyWalking、Pinpoint):无侵入监控
  • 热部署(JRebel)
  • Mock 框架(Mockito)
  • ORM 延迟加载(Hibernate)

14.12 Unsafe 类到底能做什么?

核心能力

  1. 内存操作allocateMemoryfreeMemoryputXxxgetXxx — 直接操作堆外内存
  2. CAS 操作compareAndSwapIntcompareAndSwapLongcompareAndSwapObject
  3. 线程调度parkunpark — LockSupport 底层
  4. 对象操作allocateInstance(不调用构造方法创建对象)、objectFieldOffset
  5. 内存屏障loadFencestoreFencefullFence
  6. 类操作defineClassensureClassInitialized

获取方式

  • 不能直接 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接口方法动态分派
invokedynamicLambda、方法引用动态分派

invokevirtual 执行过程

  1. 找到操作数栈顶的对象的实际类型 C
  2. 在类型 C 中查找与常量中描述符和名称匹配的方法
  3. 如果找到,进行访问权限检查,通过则返回方法引用
  4. 否则,按继承层次从下往上查找父类
  5. 始终没找到,抛出 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 prepareBinlog 写入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 的问题

  1. 所有对象一视同仁,每次 GC 扫描整个堆
  2. 短生命周期对象也需要全堆标记
  3. 浮动垃圾问题更严重(需要更多内存余量)

分代 ZGC 的改进

  • 引入年轻代和老年代的概念
  • 年轻代 GC 更频繁(minor GC),老年代 GC 较少(major GC)
  • 记忆集:记录老年代到年轻代的引用,避免扫描整个老年代
  • 写屏障从染色指针上的读屏障变为存储屏障
  • 显著降低了内存开销和 GC 频率

性能提升

  • 吞吐量提升 10%-20%
  • 更低的内存占用
  • 更短的暂停时间
  • 适合更大的堆(TB 级别)

结语

本文从 JVM 底层到分布式系统,从语言特性到框架原理,从理论到实战,全方位覆盖了 Java 技术栈的核心知识点。每个章节都力求做到:

  1. 纵向深度:从表象到底层原理,层层递进
  2. 横向对比:同类技术的全方位比较,帮助理解差异
  3. 融会贯通:知识点之间的关联和相互影响
  4. 实战导向:不仅讲原理,更讲如何应用和排查问题

"知其然,知其所以然,知其所以必然。" — 这才是真正掌握技术的标准。


本文持续更新,建议收藏并反复阅读。每一个知识点都值得深入思考和实践验证。