课堂笔记
教师:AI(DeepSeek → Claude Code) 学员:3 年+经验全栈开发工程师 这里记录每堂课的核心知识点。
第一课:CPU 缓存与 volatile
存储金字塔
| 层级 | 名称 | 速度 | 大小 | 对应物 |
|---|---|---|---|---|
| L0 | CPU 寄存器 | 0.3ns | 几十个 | 局部变量可能被 JIT 编译进寄存器 |
| L1 | L1 缓存 | 1ns | 32-64KB | CPU 私有 |
| L2 | L2 缓存 | 3ns | 256-512KB | 稍大的私有草稿本 |
| L3 | L3 缓存 | 10ns | 几 MB-几十 MB | 同 CPU 核心簇共享 |
| L4 | 主存 (RAM) | 60-100ns | 8-64GB | 内存条,Java 堆对象所在地 |
| L5 | 硬盘/SSD | 毫秒级 | TB 级 | MySQL 数据文件 |
核心认知:编程不是操作内存条,而是操作缓存副本。多线程问题的根源 90% 是因为 CPU 各自为政,拿着过期的缓存副本做计算。
volatile 关键字
两件事:
- 保证可见性:一个线程修改 volatile 变量,新值立即对其他线程可见
- 底层:Lock 前缀指令 → 强制刷回主存 + 无效化其他 CPU 缓存行
- 禁止指令重排序:编译器与 CPU 不会为了优化打乱 volatile 变量读写前后的代码顺序
- 底层:内存屏障
不能替代 synchronized:
volatile只解决”读脏数据”,不解决”读-改-写冲突”count++是三条指令:读缓存 → 加 1 → 写缓存,volatile 管不了中间的插队
volatile + 双重检查锁单例:
instance = new Singleton()三步:分配内存 → 初始化对象 → 引用指向内存- 无 volatile 时 CPU 可能重排为:分配内存 → 引用指向内存(此时对象未初始化!)→ 初始化对象
- 另一个线程拿到半成品对象直接崩溃
总结口诀:
- 可见性:volatile = 通知全班同学”板书改了”
- 原子性:synchronized = 把黑板锁起来,改完才开锁
第二课:学习策略 —— “一臂距离”原则
核心原则
只学那些:你伸出手,刚好能摸到、且明天上班调试代码时能用上的底层知识。
正确 vs 错误的学习范围
| 状态 | 例子 |
|---|---|
| ✅ 该学 | Spring Boot 自动配置原理(写 Starter) |
| ✅ 该学 | Redis RDB/AOF 持久化机制(生产丢数据) |
| ❌ 先别学 | Redis zmalloc 内存分配器实现 |
| ✅ 该学 | JVM 类加载机制(排查 ClassNotFoundException) |
| ❌ 先别学 | HotSpot C2 编译器图着色寄存器分配算法 |
技能深挖优先级(按面试频率排序)
- MySQL:行锁/间隙锁/临键锁、MVCC 与 ReadView、Change Buffer
- Redis:底层编码(Ziplist/Skiplist)、IO 多路复用单线程模型、Redlock
- JVM:对象内存布局、G1 Region 与 Remember Set、四种引用
- Spring Cloud:Nacos AP/CP 切换、Gateway WebFlux 异步非阻塞
- Vue:响应式原理(Proxy 与依赖收集)、Diff 双端比较
第三课:B+ 树索引(MySQL)
待补充——由学员提交 SQL 后引出,AI 承诺讲授
核心概念(占位)
- B+ 树按字段原始值排序
- DATE_FORMAT 等函数导致索引失效的原因
- 最左前缀匹配原则
第四课:SQL 性能分析实战
案例 SQL 五大死因
死因 1:对字段使用函数(索引粉碎机)
DATE_FORMAT(EXTENSION_APPLY_CREATE_TIME,'%Y-%m-%d') > EXTENSION_START_DATE底层:B+ 树按原始值排序,函数相当于把电话本涂改成密码再去找人 → 全表扫描
死因 2:IN (SELECT …) 子查询 底层:MySQL 5.7/8.0 可能优化为相关子查询 → 外层每扫一行,内层跑一遍 → DEPENDENT SUBQUERY
死因 3:重复子查询 相同子查询出现 10+ 次,MySQL 不会缓存,重复计算
死因 4:巨大的 IN 列表 上千个 ID 的 IN 列表 → range_optimizer_max_mem_size 超限 → 退化为全表扫描
死因 5:过多的 UNION ALL 接近 30 次 UNION ALL → 至少 30 个临时表 → 内存/磁盘 IO 爆炸
优化方案速查
| 病灶 | 手术方案 |
|---|---|
| DATE_FORMAT | 改用范围查询,或加虚拟列/函数索引(MySQL 8.0+) |
| IN (SELECT) | 改写为 EXISTS 或直接 JOIN |
| 重复子查询 | 物化到真实的中间表或 Redis 缓存 |
| 长 IN 列表 | 导入临时表后用 INNER JOIN 代替 IN |
第五课:Spring Boot Jar 包结构(“三明治”模型)
解压后的目录结构
app.jar (改名 app.zip → 解压)
├── BOOT-INF/ ← 肉馅:业务代码 + 依赖
│ ├── classes/ ← .class 字节码文件
│ └── lib/ ← 第三方依赖 jar 包
├── META-INF/ ← 面包底:启动说明书
│ └── MANIFEST.MF ← Main-Class + Start-Class
└── org/ ← 面包顶:Spring Boot 引导程序
└── springframework/boot/loader/JarLauncher.class
MANIFEST.MF 关键内容
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.yourcompany.YourMainApplication
java -jar→ 先执行 JarLauncher(第一把钥匙)- JarLauncher 启动后 → 去 BOOT-INF 找到真正的启动类(第二把钥匙)
认知升级
一个 Spring Boot 程序 = 一个精心设计的”三明治”:
[引导程序 (org/)] ← 第一片面包:负责启动和加载
[业务代码 (BOOT-INF)] ← 中间的肉和菜:你写的代码和第三方依赖
[配置文件 (META-INF)] ← 第二片面包:告诉系统怎么吃这个三明治
第六课:JVM 类加载机制与 Spring Boot 启动原理
什么是 classpath
classpath 是 JVM 启动时列出的”去哪儿找 .class 文件”的地址清单。
在 IDE 中:IDEA 自动把 target/classes/ + 所有 Maven 依赖 jar 拼成 classpath。
在命令行:java -cp myapp.jar:lib/* com.example.Main 中的 -cp 就是 classpath。
JVM 三大类加载器(双亲委派模型)
Bootstrap ClassLoader (C++ 实现) ← 加载 rt.jar (String、ArrayList 等核心类)
↑ 委托
Extension ClassLoader ← 加载 jre/lib/ext/ 下的扩展包
↑ 委托
Application ClassLoader ← 加载 classpath 下的所有类
双亲委派的含义:加载一个类时,先向上委托给父加载器,父加载器能加载就轮不到子加载器。
为什么要这样? 安全。防止你写一个 java.lang.String 替换 JDK 自带的——Bootstrap 加载器永远优先加载。
Spring Boot 的问题
Spring Boot 的 fat jar 结构里,你的业务代码在 BOOT-INF/classes/,依赖在 BOOT-INF/lib/。
Application ClassLoader 不认识 BOOT-INF 这个目录,所以无法用双亲委派正常加载。
Spring Boot 的解决方案
三步改造:
第 1 步:spring-boot-maven-plugin(打包阶段)
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>- 把
.class文件挪到BOOT-INF/classes/ - 把依赖 jar 包挪到
BOOT-INF/lib/ - 把 JarLauncher 等引导类放到
org/ - 在
MANIFEST.MF写入 Main-Class 和 Start-Class
第 2 步:JarLauncher 引导(启动阶段)
// JarLauncher 是真正的入口(由 MANIFEST.MF 的 Main-Class 指定)
public static void main(String[] args) throws Exception {
(new JarLauncher()).launch(args);
}
// 关键:isNestedArchive 筛选哪些路径需要加入类路径
protected boolean isNestedArchive(Archive.Entry entry) {
return entry.isDirectory()
? entry.getName().equals("BOOT-INF/classes/")
: entry.getName().startsWith("BOOT-INF/lib/");
}第 3 步:LaunchedURLClassLoader(加载阶段)
- 继承自
java.net.URLClassLoader - 内部维护 URL 列表:
BOOT-INF/classes/+BOOT-INF/lib/*.jar - 打破双亲委派:先在自己维护的 URL 列表里找类,找不到才让爸爸找
- 用这个 ClassLoader 加载 Start-Class,反射调用
main方法
完整的 Spring Boot 启动链条
java -jar lProject.jar
→ JVM 读 MANIFEST.MF → Main-Class = JarLauncher
→ JarLauncher.main()
→ new JarLauncher().launch(args)
→ 注册 springboot:// 协议处理器 (JarFile.registerUrlProtocolHandler())
→ ExecutableArchiveLauncher.getClassPathArchivesIterator()
→ 调用 isNestedArchive() 筛选路径
→ createClassLoader() → 创建 LaunchedURLClassLoader
→ getMainClass() → 从 MANIFEST.MF 读 Start-Class
→ Thread.currentThread().setContextClassLoader(classLoader)
→ createMainMethodRunner(launchClass, args, classLoader).run()
→ 用 LaunchedURLClassLoader 加载你的启动类
→ 反射调用 main 方法
→ 你的 Spring Boot 项目跑起来了
核心认知
Spring Boot 的 jar 包结构不是 JVM 规定的,而是 Spring Boot 自己在上面包装了一层。JVM 只认 Main-Class,后面的事全是 JarLauncher 自己用自定义 ClassLoader 搞定的。
你之前以为的”程序入口”(你自己的 LProjectApplication.main),其实是 Spring Boot 帮你手动调用的第二道入口。真正的第一道入口是 JarLauncher。