基础部分
一、java 基础与语言特性(语法/编译相关)
- java基础知识
- java基本类型的物理存储大小
- 正确理解java泛型
- 为什么局部内部类和匿名内部类只能访问final的局部变量或成员变量
- HashMap负载因子
- 反射抛出自定义异常问题
- java.sleep背后的逻辑
- java8 Lambda实现条件去重distinct List
- java-10-features
- java-11-features
- JDK12--features
- JDK 8 到 JDK 23 的语法糖和GC优化
- 关于 JDK 不同版本对 指令集支持
类加载
🌱 类加载 → 验证 → 准备 → 解析:每一步干了什么?
1. 加载(Loading)
-
JVM 根据类的全限定名(FQCN),通过类加载器读取
.class
文件字节流; -
解析 class 文件格式,构造
Class
对象; -
读取常量池(此时符号引用还未解析);
-
得到一个类的内部结构(Class 文件的结构转为 JVM 中的
Class<?>
实例);-
📦 1.
.class
文件中的结构(编译后)当你写了如下 java 类并编译后:
public class A { int x = 1; public void doSomething() {} }
编译后生成的
.class
文件是一个平台无关的二进制格式,其中包含了 JVM 执行所需的全部元信息。比如你提到的:✅ 常量池(Constant Pool)
这是 class 文件中的重中之重,里面存储了:
常量类型 含义 UTF-8 字符串 方法名、字段名、类名、描述符 Class 引用 类或接口的名字 NameAndType 方法名+方法签名 Methodref / Fieldref 方法或字段的符号引用 数值常量 int、float、String 等字面量 这些是“符号引用”,它们的含义是:“我想引用一个叫
doSomething
的方法,签名是()V
,定义在类A
中。”
-
-
类的字节流内容在此刻就常驻 JVM 内存中。
2. 验证(Verification)
- 确保 class 文件合法、安全、不被篡改;
- 包括文件结构校验、语义校验、字节码验证等;
- 与直接引用无关。
3. 准备(Preparation)
- 为类的静态字段分配内存(注意:不是对象字段);
- 并对非 final 的 static 变量赋默认值(不是赋初值!赋初值在初始化阶段做);
- 这些字段是在方法区(或 metaspace)中分配的。
这一步主要做的是内存分配,但分配的是“类级别”的静态变量,而不是“对象”。
4. 解析(Resolution)
-
将常量池中的类符号引用(ClassRef)解析成
java.lang.Class
对象; -
将方法符号引用解析为虚方法表中的实际入口;
-
将字段符号引用解析为字段的内存偏移。
-
-
这一阶段会把“常量表 + 方法表”组合起来,生成 JVM 所需的结构。
-
将 class 文件中的符号引用(Methodref、Fieldref)解析为直接引用,比如指向一个内存地址、vtable slot
-
建立方法表、虚方法表(vtable)、接口方法表(itable)
-
建立字段表,记录字段偏移和类型
-
将常量池转换为 JVM 层的
ConstantPool
对象(runtime constant pool
) -
这些结构组成了在元数据空间(jdk8以后)的InstanceKlass 结构
-
-
🌱 一、什么是符号引用?
在 .class
文件中,java 并不直接使用内存地址或者指针来表示类、字段、方法等之间的关系,而是使用符号引用(Symbolic Reference)来表示。
比如:
Foo foo = new Foo();
在字节码中表示 Foo
的时候,不是直接给一个 Foo 的地址,而是:
-
在常量池(constant_pool)中记录了类的全限定名(如
com/example/Foo
),这就是符号引用。 -
对方法的调用、字段的访问等,也都通过常量池中的索引来表示,而不是直接的内存引用。
举例:
#17 = Methodref #10.#22 // com/example/Foo."":()V
这里的 #10
是 Class_info
,里面记录的是 "com/example/Foo"
#22
是 NameAndType_info
,记录的是方法名 "<init>"
和方法描述符 "()V"
这些就是符号引用,它们都是对“名字”的引用,还没有绑定到实际的内存结构上。
-
🌱 一、什么是符号引用?
在
.class
文件中,java 并不直接使用内存地址或者指针来表示类、字段、方法等之间的关系,而是使用符号引用(Symbolic Reference)来表示。比如:
Foo foo = new Foo();
在字节码中表示
Foo
的时候,不是直接给一个 Foo 的地址,而是:- 在常量池(constant_pool)中记录了类的全限定名(如
com/example/Foo
),这就是符号引用。
- 在常量池(constant_pool)中记录了类的全限定名(如
-
对方法的调用、字段的访问等,也都通过常量池中的索引来表示,而不是直接的内存引用。
举例:
#17 = Methodref #10.#22 // com/example/Foo."":()V
这里的 #10
是 Class_info
,里面记录的是 "com/example/Foo"
#22
是 NameAndType_info
,记录的是方法名 "<init>"
和方法描述符 "()V"
这些就是符号引用,它们都是对“名字”的引用,还没有绑定到实际的内存结构上。
🌿 二、什么是直接引用?
在类被加载、链接之后,JVM 会把这些符号引用解析成“直接引用”:
- 指向方法区中已经加载好的类结构(如
Class
对象、方法表、字段表);- 或者是指向运行时数据结构的地址、偏移量、方法入口等。
直接引用就是可以直接用于运行的内存指针或偏移。
解析会:
- 查找符号引用所指向的类、字段、方法等是否已经被加载;
- 如果没有,则触发加载;
- 然后将这些符号引用替换成运行时结构的直接引用。
比如:
ClassRef
→ 变成java.lang.Class
对象的指针;FieldRef
→ 变成偏移量或者字段表中的地址;MethodRef
→ 变成方法表中的地址或 native 方法入口。
总结一句话:解析就是让编译期的“名字”,在运行期指向真实的结构。
🌾 四、解析举例(图解)
比如下面 java 代码:
Person p = new Person();
p.sayHello();
在字节码中可能是这样:
new #2 // #2 是类 com/example/Person 的符号引用
invokespecial #3 // #3 是方法 <init>:()V 的符号引用
invokevirtual #4 // #4 是 sayHello:()V 的符号引用
在解析阶段:
#2
被解析成方法区中的 Person 类结构的指针;#3
被解析成Person.<init>()
构造函数的入口;#4
被解析成sayHello()
方法表中的偏移或函数指针。
🌻 五、何时解析发生?
JVM 规范允许解析发生在类加载时,也允许懒解析(即用到再解析)。
JVM 有一些策略:
- 主动解析:类初始化时把所有常量池里的引用都解析掉;
- 懒解析(延迟绑定):用到某个符号引用时再解析。这个策略用得比较多(提升启动性能)。
例如调用某个类的静态方法时,如果还没解析该方法符号引用,则会在这个时刻进行解析。\image-20250804162539998.png)
🌿 二、什么是直接引用?
在类被加载、链接之后,JVM 会把这些符号引用解析成“直接引用”:
- 指向方法区中已经加载好的类结构(如
Class
对象、方法表、字段表);- 或者是指向运行时数据结构的地址、偏移量、方法入口等。
直接引用就是可以直接用于运行的内存指针或偏移。
解析会:
- 查找符号引用所指向的类、字段、方法等是否已经被加载;
- 如果没有,则触发加载;
- 然后将这些符号引用替换成运行时结构的直接引用。
比如:
ClassRef
→ 变成java.lang.Class
对象的指针;FieldRef
→ 变成偏移量或者字段表中的地址;MethodRef
→ 变成方法表中的地址或 native 方法入口。
总结一句话:解析就是让编译期的“名字”,在运行期指向真实的结构。
🌾 四、解析举例(图解)
比如下面 java 代码:
Person p = new Person();
p.sayHello();
在字节码中可能是这样:
new #2 // #2 是类 com/example/Person 的符号引用
invokespecial #3 // #3 是方法 <init>:()V 的符号引用
invokevirtual #4 // #4 是 sayHello:()V 的符号引用
在解析阶段:
#2
被解析成方法区中的 Person 类结构的指针;#3
被解析成Person.<init>()
构造函数的入口;#4
被解析成sayHello()
方法表中的偏移或函数指针。
🌻 五、何时解析发生?
JVM 规范允许解析发生在类加载时,也允许懒解析(即用到再解析)。
JVM 有一些策略:
- 主动解析:类初始化时把所有常量池里的引用都解析掉;
- 懒解析(延迟绑定):用到某个符号引用时再解析。这个策略用得比较多(提升启动性能)。
例如调用某个类的静态方法时,如果还没解析该方法符号引用,则会在这个时刻进行解析。
✅ 整体流程理解图示(类 → 对象):
+-----------------+
class文件 --> | 符号引用(常量池)|
+-----------------+
↓ 加载
+-----------------+
| Class<?>结构 | ← 类加载器构造的类元数据结构
+-----------------+
↓ 准备
+-----------------+
| 静态变量内存 |
+-----------------+
↓ 解析
+--------------------------+
| 常量池中的引用 → 指针/入口 |
+--------------------------+
↓ 初始化
+--------------------------+
| 赋初值 / 执行<clinit>() |
+--------------------------+
↓ new
+-------------------+
| 堆中分配对象空间 | ← 这一步才是创建实例
+-------------------+
二、JVM 与内存管理
1. 内存结构 & 对象模型
思考
1.volatile 在java中起到lock的作用,就是读取对应内存值的时候不读取寄存器的值,根据mesi协议,如果遇到这个内存的值,是不是一定读取内存的值,而不是直接读取1,2级缓存的值
✅ 一、volatile
是不是等价于 lock?
不是。
特性 | volatile |
synchronized / lock |
---|---|---|
可见性 | ✅ 有 | ✅ 有 |
有序性(禁止指令重排) | ✅ 有(只针对该变量) | ✅ 更强(全域) |
原子性 | ❌ 没有 | ✅ 有 |
对应机器指令 | lock 前缀指令(如 lock cmpxchg ) |
lock 伪指令+monitor |
volatile
只保证对该变量的 读写是有序且立即可见的,但不保证原子性。
🧠 二、volatile
的作用是啥?
java 编译器和 JVM 在遇到 volatile
变量时,会生成特殊的汇编指令来保证 内存可见性 和 禁止指令重排序:
- 写操作后,会插入
store-store
、store-load
内存屏障(Memory Barrier); - 读操作前,会插入
load-load
、load-store
屏障。
而这些指令,通常在底层 x86 CPU 上使用类似于:
lock xchg
或者是强制使用 MFENCE
/LFENCE
/SFENCE
指令。
🔄 三、MESI 协议和缓存一致性
####### ✅ 什么是 MESI?
CPU 的缓存一致性协议,四种状态:
状态 | 名称 | 描述 |
---|---|---|
M | Modified | 缓存为修改态,值未写回内存,独占 |
E | Exclusive | 缓存为独占态,值与内存一致,只有自己有 |
S | Shared | 缓存为共享态,与其他核心共享,未修改 |
I | Invalid | 缓存为无效态,不可读 |
🎯 你的核心问题:
当有
volatile
修饰变量时,如果某个线程读取这个变量,是不是一定会从主内存读取,而不会使用 L1/L2 缓存?
####### ❌ 答案是否定的 —— 不是一定读主内存。
✅ 真相:会读缓存(但要确保是最新的)
####### volatile
并不会让每次访问都跳过缓存直接去主内存。
那样太慢,根本无法接受。
####### 正确做法是:
- 通过内存屏障 + MESI 协议,确保你读取的缓存内容是「最新的」。
🧵 举个真实场景:
线程 A 写了 volatile x = 1
,线程 B 读它。
- A 写的时候触发:
lock
指令或store-load barrier
;- 让该缓存行进入 M 状态,并广播 Invalidate 到其他核心,强制 B 的缓存行变为 I 状态。
- B 读的时候:
- 发现自己缓存中 x 的行是 I 状态(Invalid);
- 触发从其他核心或主内存中拉取最新数据;
- 拉取后放入自己的缓存行,可能以 S 状态或 E 状态存储。
####### 关键点:
B 最终读取的数据是最新的,但它最终仍然是存在 L1/L2 缓存中的,只是这个数据是有效且可见的。
✅ 总结你的问题答案:
问题 | 回答 |
---|---|
volatile 是否起到 lock 的作用? |
❌ 不起到原子性的 lock 作用,只保证可见性和有序性 |
使用 volatile 变量时是否会跳过缓存? | ❌ 不会直接跳过,而是通过缓存一致性机制使缓存行最新 |
是否每次都从主内存读取? | ❌ 否,读取仍是从缓存,但缓存必须是最新有效的(非 Invalid) |
volatile 如何实现内存可见性? | ✅ 通过内存屏障 + MESI 协议使缓存失效或同步 |
📌 补充小结(一图概括)
java volatile -> 内存屏障 + 禁止重排序
↓
CPU 执行 lock 或 fence 指令
↓
使其他核心的缓存行失效(MESI协议)
↓
其他线程读取时重新拉取最新值(缓存or内存)
↓
✅ 保证可见性,不等于总是读主内存
2.java 内置锁 在锁定静态方法的时候锁定的Class对象,那么Class对象是否存在堆中,如果是,那Class里面的数据是怎样的
1. Class 对象在 JVM 内存中的位置
- 类元数据(method area / metaspace)
存放的是:- 常量池(runtime constant pool)
- 字段信息(field metadata)
- 方法信息(method metadata)
- 方法字节码(code array)
- 静态变量(static fields)
- 类加载器引用、父类引用等结构
- java 堆中有一个对应的
java.lang.Class
实例- 每当 JVM 加载一个类(通过类加载器),都会在堆上创建一个
Class
类型的对象来代表它。 - 这个
Class
实例里并不直接保存所有元数据,而是持有一个指针/引用指向元空间中的类结构。 - 程序中
MyClass.class
、obj.getClass()
拿到的就是这个 Class 对象。
- 每当 JVM 加载一个类(通过类加载器),都会在堆上创建一个
换句话说:
堆中: Class对象(锁的实际对象)
↓
元空间:类的结构化信息(字段、方法、常量池等)
2. 锁定静态方法时锁的是哪儿
静态同步方法:
public static synchronized void foo() { ... }
编译后字节码会在 ACC_SYNCHRONIZED
标志位上注明需要同步,执行时:
- 进入方法前,JVM 会使用
MyClass.class
这个对象作为锁的监视器(monitor)。 - 因为
Class
对象是堆中的一个普通对象,所以锁定操作是通过堆对象的对象头(mark word)来完成的。 - 这样同一个类的静态方法竞争时,都是竞争同一个
Class
实例的监视器。
3. Class 对象内部数据
Class
对象的 java 层属性(java.lang.Class
)包含:
private final ClassLoader classLoader
— 关联类加载器private final Class<?> componentType
— 如果是数组类,这里存数组元素类型private final int modifiers
— 访问修饰符private final String name
— 类的全限定名- 其他反射用的结构(方法、字段、构造器缓存)
但是这些信息大部分并不是直接存 Java 字段里,而是通过JNI / JVM 本地代码去访问元空间中的类元数据结构(Klass 结构)。
Klass 结构(HotSpot 中的实现)里存放:
- 常量池指针
- 字段表指针
- 方法表指针
- 接口表
- 类加载器引用
- 超类引用
- 静态字段的存储位置指针
- vtable(虚方法表)
4. 总结
Class
对象本身在堆中,是 Java 程序可以访问到的对象。- 它内部的“真正的类定义信息”在元空间(JDK 8+)或方法区(JDK 7 及以前)。
- 锁定静态方法时,锁的就是堆上的 Class 对象,通过这个对象的对象头实现 monitor 进入/退出。
- 元数据(方法、字段、常量池等)不在堆上,而是被
Class
对象间接引用。
5.示意图
┌───────────────────────┐
│ java 堆 │
└───────────────────────┘
│
▼
┌─────────────────────┐
│ Class 对象实例 │ ←─ MyClass.class / obj.getClass()
│ (java.lang.Class) │
├─────────────────────┤
│ classLoader →───────┼──► ClassLoader 实例
│ name → "MyClass"
│ modifiers → public
│ ... → 反射缓存
│ (方法/字段信息的引用缓存)
│
│ nativePtr →─────────────┐
└─────────────────────┘ │
│
┌─────────────────────────┘
▼
┌────────────────────────────┐
│ 元空间 (Metaspace) │
└────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Klass 结构 │ ← HotSpot 内部类元数据对象
├────────────────────────────┤
│ 常量池指针 (ConstantPool*) │
│ 字段表(Field Info Table) │
│ 方法表(Method Info Table) │
│ 接口表(Interfaces) │
│ 父类引用(SuperKlass*) │
│ 静态字段存储区(Statics) │
│ 虚方法表(vtable) │
└────────────────────────────┘
解释
- 堆
- JVM 为每个被加载的类生成一个
java.lang.Class
对象,存在堆中。 - 静态方法
synchronized
时,锁的就是这个对象(对象头里的 monitor)。
- JVM 为每个被加载的类生成一个
- 元空间 (Metaspace)
- 存放类的“真正定义”,即 HotSpot 内部的 Klass 结构。
- 包含:常量池、字段信息、方法信息、接口表、继承信息、静态变量的存放地址等。
- Class 对象与 Klass 的关系
- Class 对象里有一个
nativePtr
(或类似的 JVM 内部字段),指向元空间中的 Klass 结构。 - 程序通过
Class
访问方法/字段时,其实是去 Klass 中查找,然后可能缓存到 Class 对象里。
- Class 对象里有一个
- 锁定静态方法时
- JVM 找到
Class
对象(堆中实例),对它的 对象头(mark word) 做 monitor enter/exit 操作。 - 不会直接锁元空间里的 Klass 结构。
- JVM 找到
2. GC 垃圾回收
思考:jvm在做垃圾回收的时候使用的是GC Root的方式,那么能够作为GC Root的对象或者引用是在执行GC的时候遍历得到的还是存在某个地方
✅ GC Roots 的集合本身是 JVM 内部固定管理的一些特殊引用点,它们一直都“存在于某些特定区域中”,在 GC 开始时作为起点被遍历。
一、哪些是 GC Roots?
GC Roots 并不是“运行时动态计算出来的”,而是 JVM 在设计时就规定好的一些特殊位置中的引用,只要对象被这些引用直接或间接关联,就会被认为是“可达对象”。
常见的 GC Roots 包括:
类型 | 说明 |
---|---|
栈帧中的本地变量表(Local Variables) | 所有线程正在执行的方法调用栈中,局部变量表引用的对象。 |
方法区中的类静态属性引用的对象 | 比如 static 引用的对象。 |
方法区中常量引用的对象 | 比如字符串常量池中的引用。 |
本地方法栈中 JNI 引用的对象 | native 方法使用的对象引用。 |
虚拟机内部的一些系统类引用的对象 | 比如 ClassLoader、线程对象等。 |
被同步锁(monitor)持有的对象 | 即进入 synchronized 的 monitor 对象。 |
这些地方都是 JVM 内部结构中预定义的“根节点集合”,它们:
- 不会被 GC 回收;
- 在 GC 开始时 由 GC Root Set 构成“起始点”,从这些点开始做“可达性分析”。
二、GC Root 本身是怎么被 JVM 管理的?
它们 不是“临时生成”或“遍历所有对象得出” 的,而是:
- 存在于 JVM 内部的线程栈、方法区、堆栈边界、JNI 表等结构中;
- JVM 一开始就知道哪里能找到这些引用点;
- 比如每个线程的栈帧、方法区中的静态表、常量池表等,都是固定结构,JVM 实现里知道如何访问。
三、GC 的过程简化流程:
- 构建 GC Root Set:从 JVM 结构中直接获取“GC Roots”集合,比如所有线程的栈帧、本地方法引用等。
- 进行可达性分析(mark):从这些 GC Root 出发,遍历引用链,把所有可达对象打上标记。
- 清理不可达对象(sweep):没有被标记的就会被认定为垃圾,进行回收。
四、一个形象类比:
可以把 GC Roots 理解为一个“森林里所有的井口”,这些井口是 JVM 早就规划好的,GC 时从这些井往下打水(遍历对象图),能接到水的对象是活的,接不到的就是垃圾。
总结:
- GC Root 并不是遍历所有对象得到的,而是 存在于 JVM 固定结构中的引用点集合。
- JVM 在 GC 时 直接访问这些已知的位置,作为遍历起点。
- GC 的“可达性分析”是从这些 GC Root 出发,构建出“活对象集合”的过程。
3. JVM 工具 & 调优实例
4. 高并发相关
三、并发与ThreadLocal机制
🧵 ThreadLocal 原理与内存泄漏机制
ThreadLocal
是 java 提供的一种线程隔离机制,它通过为每个线程维护变量的副本,实现变量在多线程中的独立性和线程安全性,无需显式加锁。
✅ 原理概述:线程实例副本
每个线程 Thread
对象内部都维护着一个 ThreadLocalMap
实例,该 Map 的 key 是 ThreadLocal
对象本身,value 是线程为该变量存储的副本值。因此:
- 不同线程访问同一个
ThreadLocal
变量,实际访问的是不同线程中的独立副本; - 各线程之间相互隔离,互不干扰。
🚨 为什么会内存泄漏?如何解决?
由于 ThreadLocalMap
的 key 是对 ThreadLocal
的 弱引用(WeakReference),一旦没有外部强引用指向某个 ThreadLocal
实例,它就会被 GC 回收。
但 ThreadLocalMap.Entry
中的 value 是强引用,所以:
当 key 被回收后,value 仍被当前线程持有,无法被 GC 清除 —— 这就导致了内存泄漏。
🔍 示例:ThreadLocalMap 的 set()
方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); // 弱引用 get()
if (k == key) {
e.value = value; // 更新
return;
}
if (k == null) {
// key 已被 GC 回收,此时 Entry 已过期,进行替换清理
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); // 新增 Entry
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 扩容并触发过期 Entry 的清理
}
💡 如何防止内存泄漏?
ThreadLocalMap 通过以下机制自动清理过期 Entry:
- 弱引用机制:key 被 GC 回收后,get() 返回 null,可识别为过期;
- 清理策略:
replaceStaleEntry()
:替换和清除已过期的 Entry;cleanSomeSlots()
和expungeStaleEntry()
:批量扫描并清理无效 Entry;
- 建议: 显式调用
ThreadLocal.remove()
清除变量,尤其在使用线程池等线程复用场景中,避免数据残留。
📍 使用场景一:每个线程需要一个独立的实例
例如数据库连接、用户上下文、事务信息等需要线程隔离的数据。ThreadLocal
提供了优雅的方式,无需每次手动创建实例。
📍 使用场景二:线程内多方法共享数据,但线程间隔离
在框架设计中非常常见,如:
- Spring 中的
RequestContextHolder
- MyBatis 中的事务管理器
- 日志记录中的 traceId 传递
使用 ThreadLocal
可避免通过方法参数层层传递,提升代码可读性与复用性。
🧠 总结
特性 | 说明 |
---|---|
隔离性 | 每个线程有独立副本,互不影响 |
弱引用 | key 是 WeakReference,防止强引用泄漏 |
内存泄漏风险 | key 回收但 value 未清理,需主动调用 remove() |
应用场景 | 用户上下文、数据库连接、traceId 等线程独立数据 |
ThreadLocal
是一种优秀的线程封装机制,但如果使用不当,容易引发隐式内存泄漏问题。理解底层结构与使用约束,有助于更安全、高效地应用它。
java 静态锁的实现机制:Class 对象作为锁标记
✅ 本质:使用 Class
对象作为锁
在 java 中,静态同步方法或静态代码块的锁对象是对应类的 Class
对象。
示例说明:
public class Demo {
public static synchronized void staticMethod() {
// synchronized(Demo.class)
}
}
等价于:
synchronized(Demo.class) {
// 静态方法锁,作用于类级别
}
✅ 锁粒度说明:
- 静态方法加锁 → 锁的是类(
Class
对象) → 全部实例共享; - 实例方法加锁 → 锁的是当前对象实例 → 每个实例互不影响;
Class
对象是类加载到 JVM 时创建的唯一对象,天然具备互斥标识能力,适合作为静态锁。
wait()方法为什么要放在Object类中
简单说:因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object。
专业说:因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在object类中
为什么 MESI 协议并不足以保证 java 中的“可见性”语义?
四、网络与I/O相关
五、设计模式
- 洞察设计模式的底层逻辑
- 模板方法、策略、观察者、适配器、单例、工厂
六、JVM相关图解 & 书籍笔记
- JVM 图解
-
java性能权威指南学习笔记
Spring系列
1. IoC 原理与实现机制详解
Spring 的 IoC(控制反转)机制核心在于:通过容器统一管理对象的生命周期和依赖关系,实现应用层与对象创建过程的解耦。
整个 IoC 容器的初始化和 Bean 的创建过程主要包括以下几个关键阶段:
(1)创建容器实例
Spring 首先通过 BeanFactory
或其实现类(如 DefaultListableBeanFactory
)创建一个核心容器,负责管理 Bean 的注册、创建与销毁等。
(2)注册 BeanDefinition(元数据)
Spring 会通过注解或 XML 配置,将每个 Bean 的元数据信息(BeanDefinition
)注册到容器中。
这些元数据包括:
- Bean 的类类型
- 是否为单例
- 依赖的其他 Bean 名称
- 初始化和销毁方法等
(3)配置 BeanFactory 的扩展能力
接下来,容器会执行一系列 BeanFactoryPostProcessor
接口的实现类,用于在 Bean 实例化之前对 BeanDefinition
做进一步的处理。
例如:
PropertySourcesPlaceholderConfigurer
:处理${}
占位符,加载配置文件;ConfigurationClassPostProcessor
:处理@Configuration
、@ComponentScan
等注解,注册额外的 Bean。
这一步非常关键,它实现了容器的“自我增强”和动态 Bean 注册。
(4)注册 BeanPostProcessor(对象级扩展)
在实例化 Bean 之前,容器会注册一系列 BeanPostProcessor
实例,这些处理器负责在 Bean 初始化前后做一些增强工作,如:
- Aware 接口(如
BeanNameAware
,ApplicationContextAware
)注入容器上下文; - 自动代理(如 AOP);
- @Autowired/@Value 等依赖注入注解解析。
(5)实例化 Bean(反射构造)
此时容器会使用反射(调用构造器)将 BeanDefinition
转换为具体的 Bean 实例。
如果有构造函数参数或依赖注入,也会在这一步完成解析并注入。
(6)初始化过程(增强与生命周期回调)
在 Bean 被构造完成后,Spring 会依次执行以下步骤完成初始化:
- 属性填充(依赖注入)
- 调用
Aware
接口(注入容器信息) - 调用
BeanPostProcessor.postProcessBeforeInitialization()
- 调用初始化方法(如
@PostConstruct
、InitializingBean.afterPropertiesSet()
) - 调用
BeanPostProcessor.postProcessAfterInitialization()
(7)完成 Bean 的注册
容器将创建完成、初始化完毕的 Bean 缓存到容器中(如单例池),完成一次完整的生命周期初始化。
(8)销毁流程(容器关闭时)
当容器关闭时,会:
- 调用
DisposableBean.destroy()
; - 或执行用户配置的
@PreDestroy
/destroy-method
; - 清理 Bean 缓存及相关资源。
2. Spring Boot 启动时加载热点数据到 Redis 的实现机制
在理解了 IoC 原理之后,我们可以进一步讨论一个非常实际的场景:如何在 Spring Boot 启动时将数据库中的热点数据加载到 Redis 中。
这个问题看似简单,实际上需要准确掌握 Spring 容器启动流程和 Bean 生命周期的执行顺序。特别是要确保:
加载 Redis 的逻辑在数据源已初始化之后,并且尽可能晚一点执行,避免依赖未准备好。
2.1 常见实现方式及时机选择
机制 / 注解 | 执行时机 | 是否适合加载数据 |
---|---|---|
@PostConstruct |
Bean 创建完成后立即执行 | ❌ 数据源可能未准备好 |
InitializingBean.afterPropertiesSet() |
与 @PostConstruct 相同 |
❌ 同样过早 |
ApplicationRunner / CommandLineRunner |
所有 Bean 初始化完成后,容器启动最后阶段 | ✅ 最推荐 |
@DependsOn |
指定依赖的 Bean 必须提前实例化 | ✅ 控制顺序辅助用 |
@Order |
Runner 执行顺序控制(数字越小越先执行) | ✅ 多个 Runner 时使用 |
延迟 Bean 注入 | 在执行逻辑中通过注入方式触发依赖实例化 | ✅ 防止提前初始化 |
2.2 推荐方式:使用 ApplicationRunner
加载热点数据
@Component
@Order(1) // 决定多个 Runner 的执行顺序
public class CachePreloadRunner implements ApplicationRunner {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private HotDataService hotDataService;
@Override
public void run(ApplicationArguments args) throws Exception {
List<HotItem> hotItems = hotDataService.loadFromDb();
for (HotItem item : hotItems) {
redisTemplate.opsForValue().set(item.getKey(), item);
}
System.out.println("热点数据已加载到 Redis");
}
}
ApplicationRunner
与CommandLineRunner
的唯一区别是参数类型不同,前者提供解析后的参数结构。
2.3 使用 @DependsOn
控制初始化顺序(可选)
如果你的热点数据加载逻辑依赖某个特定的 Bean(如数据源、配置中心初始化),可以配合使用 @DependsOn
来确保顺序:
@Component
@DependsOn("dataSource")
public class CachePreloadRunner implements ApplicationRunner {
// ...
}
2.4 加深理解:它与 IoC 有何关系?
Spring Boot 启动时的整个数据加载流程,其实是 IoC 控制的一个典型延伸:
- 所有对象(如 RedisTemplate、Service、DataSource)都交由容器统一创建;
- 加载逻辑的执行时机,也不是我们主动调用,而是容器在合适时机反转控制权来执行;
- 每一步都体现出容器主导 → 应用被动响应的 IoC 精神。
👉 延伸阅读:
3. Spring 中 Bean 的完整生命周期解析
在 Spring 容器的管理下,每一个 Bean 的创建、初始化、使用直至销毁都遵循着一套明确的生命周期流程。理解这一过程对于掌握 IoC 容器的原理和扩展点至关重要。
下面是 Spring 容器中 Bean 生命周期的完整流程解析:
(1)实例化 Bean(Instantiation)
Spring 使用反射机制调用无参构造方法或指定构造器,创建一个空的 Bean 实例。此时对象已经存在于内存中,但尚未注入任何属性。
- 关键方法:
instantiateBean()
或构造器反射 - 目的:创建原始对象,不带依赖和配置
(2)属性填充(Dependency Injection)
容器调用 populateBean()
方法,根据 BeanDefinition
中的依赖信息为 Bean 注入属性:
- 注入其他 Bean(构造函数 / setter / 字段)
- 注入配置值(如
@Value
) - 此步骤中,Spring 会使用 三级缓存机制 处理 循环依赖,即:
- 一级缓存:单例池(完全初始化好的 Bean)
- 二级缓存:早期暴露的 Bean(提前暴露的对象引用)
- 三级缓存:用于创建代理对象(如 AOP)
循环依赖能被解决的前提:必须是“单例 + 非构造器注入”
(3)调用 Aware 接口(容器感知)
如果 Bean 实现了以下接口之一,容器会在此阶段调用对应的方法:
BeanNameAware
:注入当前 Bean 的名称BeanClassLoaderAware
:注入类加载器BeanFactoryAware
:注入 BeanFactory 引用
这些接口提供了 让 Bean 感知自身在容器中的运行环境 的能力。
(4)执行 BeanPostProcessor 的前置处理
在 Bean 初始化之前,Spring 会遍历所有实现了 BeanPostProcessor
接口的类,执行其 postProcessBeforeInitialization()
方法。
此处可以做很多事情,例如:
- 注入
ApplicationContext
、Environment
等核心容器信息(由ApplicationContextAwareProcessor
实现) - 自定义处理逻辑,比如修改属性、记录日志等
(5)执行初始化逻辑
Spring 会检查 Bean 是否实现了以下两种初始化方式:
- 是否实现
InitializingBean
接口,如果有,则调用其afterPropertiesSet()
方法; - 是否配置了自定义
init-method
,若有则反射调用。
通常推荐使用
@PostConstruct
注解作为标准初始化入口。
(6)执行 BeanPostProcessor 的后置处理(核心扩展点)
这是最重要的扩展点之一。
Spring 会调用所有 BeanPostProcessor
的 postProcessAfterInitialization()
方法。此阶段是 Spring AOP 织入代理的关键位置,如:
AbstractAutoProxyCreator
及其子类会通过wrapIfNecessary()
判断是否需要为当前 Bean 创建代理对象;- 如果需要,会使用 JDK 或 CGLIB 生成动态代理对象替换原始 Bean。
这一步决定了我们平时看到的 @Transactional
、@Async
等是否生效。
(7)完成初始化,放入容器
此时 Bean 已被完全初始化,容器会将其加入单例池(如果是 singleton),随后可以通过 getBean()
正常获取。
(8)销毁(Destruction)
当容器关闭(如 Spring 应用关闭、Context 关闭)时,会执行 Bean 的销毁流程。
Spring 会依次检查:
- 是否实现
DisposableBean
接口,调用其destroy()
方法; - 是否配置了
destroy-method
,则反射调用; - 是否使用
@PreDestroy
注解标记了销毁方法。
此外,还会调用注册在 DestructionAwareBeanPostProcessor
中的销毁回调钩子。
🔁 生命周期流程简图
实例化
↓
依赖注入(populateBean)
↓
Aware 接口回调
↓
BeanPostProcessor 前置处理
↓
初始化(init-method / afterPropertiesSet)
↓
BeanPostProcessor 后置处理(AOP代理)
↓
完成初始化 → 加入单例池 → getBean
↓
(容器关闭时)
↓
销毁方法执行
4. Spring 如何解决循环依赖?
在 Spring 中,循环依赖是指 Bean A 依赖 Bean B,而 B 又依赖 A,形成一个“闭环”。如果不加以处理,系统在初始化 Bean 时就会陷入死循环。Spring 通过 三级缓存机制 有效解决了“非构造器注入的循环依赖问题”。
4.1 循环依赖的关键点
- 场景:A → B → A,setter 或字段注入时触发
- 限制:只能解决 singleton 且非构造器注入的依赖
- 核心机制:三级缓存 + 提前暴露 + AOP 延迟代理
4.2 三级缓存详解
Spring 在 DefaultSingletonBeanRegistry
中维护三层缓存,分别管理 Bean 的不同状态:
缓存名称 | 描述 |
---|---|
singletonObjects(一级) | 完整初始化后的 Bean 实例 |
earlySingletonObjects(二级) | 允许提前暴露的“不完整 Bean”(未被 AOP 代理) |
singletonFactories(三级) | 用于延迟创建 Bean 实例的 ObjectFactory ,可用于创建代理对象 |
4.3 生命周期中三级缓存的作用流程:
- createBeanInstance 阶段:将
ObjectFactory
放入三级缓存; - populateBean 阶段:若依赖对象未完成初始化 → 从三级缓存获取对象 → 判断是否需要 AOP → 放入二级缓存;
- initializeBean 阶段:完成初始化后 → 放入一级缓存,清除二级、三级缓存。
ObjectFactory 是一个函数式接口,用于延迟创建 Bean,有利于解决代理对象还未生成但又需要引用自身的情况。
👉 延伸阅读:
5. Spring AOP 的实现原理
Spring 的 AOP 是 IOC 容器的自然拓展,依赖于 Bean 的生命周期,在创建过程中通过 动态代理机制 实现方法级别的增强。
5.1 AOP 主要机制:
- 切面(Aspect):横切逻辑,如日志、事务、安全等
- 切点(Pointcut):定义在哪些方法上增强
- 通知(Advice):增强逻辑
- 织入(Weaving):增强逻辑与目标类的结合过程
5.2 关键实现流程:
- Bean 初始化过程中,
AbstractAutoProxyCreator
作为BeanPostProcessor
的实现类,在postProcessAfterInitialization()
中执行代理逻辑; - 判断该 Bean 是否符合切点 → 如果是,使用 JDK 或 CGLIB 生成代理;
- 代理执行逻辑由
DynamicAdvisedInterceptor
的intercept()
方法驱动。
👉 延伸阅读:
6. Spring 事务的回滚原理
Spring 的声明式事务功能本质上是通过 AOP 实现的,其核心实现类是 TransactionInterceptor
,用于拦截事务方法并进行事务控制。
6.1 执行流程如下:
- 拦截方法调用
- 获取事务属性配置(如传播级别、回滚规则等)
- 开启事务(关闭自动提交,保存状态)
- 执行目标方法
- 方法抛异常时回滚:
doRollback()
,通过completeTransactionAfterThrowing()
实现; - 方法正常返回时提交:
doCommit()
,通过completeTransactionAfterReturning()
实现; - 清理资源:
cleanupTransactionInfo()
,解绑线程变量
7. 事务传播机制(Propagation)
Spring 允许定义方法间事务行为的传播方式,控制事务边界的传递与隔离,常见传播行为包括:
REQUIRED
:默认,当前有事务则加入,没有则新建REQUIRES_NEW
:无论是否有事务,都新建一个NESTED
:嵌套事务,依赖保存点实现回滚SUPPORTS
、NOT_SUPPORTED
等
事务传播行为是多模块协作时的重要保障,避免事务混乱或丢失。
8. BeanFactory 与 FactoryBean 的区别
对比项 | BeanFactory | FactoryBean |
---|---|---|
作用 | Spring 的核心容器接口 | 用于用户自定义生成 Bean 的工厂类 |
复杂性 | 高,涉及完整生命周期 | 简洁,适合封装复杂对象创建逻辑 |
使用场景 | IoC 容器顶层控制接口 | 封装第三方对象(如 MyBatis 的 SqlSessionFactory) |
接口方法说明 | 无 | getObject() ,isSingleton() ,getObjectType() |
9.spring 注入
10.Spring请求过程
11. Spring 中常见设计模式
Spring 本质上是“设计模式的集大成者”,框架本身大量使用并暴露以下经典设计模式:
设计模式 | 应用示例 |
---|---|
单例模式 | 所有默认 Bean 为单例,统一管理实例 |
工厂模式 | BeanFactory、FactoryBean |
模板方法 | AbstractBeanFactory 定义创建流程并允许拓展 |
观察者模式 | ApplicationEvent + Listener |
策略模式 | PropertyEditor、Resource 解析器 |
适配器模式 | HandlerAdapter(Spring MVC 请求适配) |
代理模式 | JDK / CGLIB 动态代理,用于实现 AOP 与事务 |
12. 多系统数据库字段加密的实践方案
在实际开发中,可能会遇到以下场景:多个系统共用或独立的数据库中存在敏感字段(如身份证、手机号、银行卡号等),需要在不影响业务功能的前提下完成批量加密。推荐使用 MyBatis 的插件机制进行无侵入式处理。
✅ 技术方案:MyBatis 拦截器
- 核心思路:在执行
INSERT
和UPDATE
操作时拦截 SQL 参数,对敏感字段进行加密处理; - 实现方式:
- 实现
Interceptor
接口; - 拦截
Executor.update()
方法; - 通过
MetaObject
操作参数对象中的字段; - 判断字段是否需要加密,如
@Sensitive
注解标识; - 使用 AES、RSA 等算法加密;
- 替换入参后继续执行。
- 实现
🧩 注意事项:
- 建议配合字段注解 + 加密组件(如 Hutool、Jasypt);
- 大批量加密推荐脱离业务主流程,在启动时批处理或工具脚本;
- 解密可以在查询拦截器中处理,或者封装在 service 层逻辑中。
🌐 服务发现与远程调用
在微服务架构中,各个服务往往是解耦部署的,需要通过 HTTP 或 RPC 方式相互调用。Spring Cloud 提供了多个组件来支撑这一能力,典型代表包括:Feign
(声明式调用)、Ribbon/LoadBalancer
(负载均衡)、Sentinel
(熔断限流)等。
配置中心架构设计:Pull vs Push
在大型系统中,配置中心承担着下发和统一管理配置的职责,Pull 与 Push 是两种核心的下发模型,各有适用场景。
✅ Pull(拉取)模型:
- 特点:客户端主动定时或按需从配置中心获取配置;
- 优点:
- 实现简单,易于控制;
- 容错性强:服务端宕机不会影响已获取配置;
- 缺点:
- 不实时;
- 配置更新后需要等待下一个周期才能生效。
✅ Push(推送)模型:
- 特点:配置中心在配置变更后立即推送到客户端;
- 优点:
- 实时性强,配置修改可秒级生效;
- 缺点:
- 服务端压力大;
- 容错性差,客户端需维护长连接、重连等逻辑。
✅ 选择依据:
维度 | 推荐模型 |
---|---|
配置变更频繁、需要秒级生效 | Push |
系统规模较小、变更少 | Pull |
高并发系统(如支付) | Pull + 本地缓存 |
混合方案(如 Nacos) | 支持 Push + Failover Pull |
实践建议:在设计配置中心时,可以采用 Push 为主、Pull 为辅(兜底) 的混合模式,兼顾实时性与稳定性。
🔗 @FeignClient
:声明式 HTTP 客户端
上一节已详细介绍
@FeignClient
注解,这里作为模块首节重点展开,帮助读者理解如何通过注解实现“远程服务调用的本地封装”。
关键特性:
- 自动构建接口代理对象
- 自动集成负载均衡与熔断(如果引入 Ribbon、Sentinel)
- 可通过 fallback/fallbackFactory 进行容错处理
- 支持 URL 调试与配置解耦
🎯 负载均衡机制:Ribbon vs LoadBalancerClient
✅ Ribbon(已过时):
-
Netflix 提供的客户端负载均衡器,已停止维护;
-
被 Spring Cloud
LoadBalancer
模块所替代; -
配合 Feign 使用非常常见(早期 Feign 默认集成 Ribbon);
-
核心配置包括:
user-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
✅ Spring Cloud LoadBalancer(推荐):
- Spring 官方提供的新一代客户端负载均衡组件;
- 依赖
spring-cloud-starter-loadbalancer
; - 支持更强扩展性,基于
Reactor
构建,支持响应式编程; - Feign 在 Spring Cloud 2020+ 中默认集成它而非 Ribbon。
🛡️ Sentinel:熔断限流保护
Sentinel 是阿里开源的流量控制组件,集成到 Feign 可实现远程调用的熔断与自定义处理。
整合方式:
-
引入依赖:
com.alibaba.cloud spring-cloud-starter-alibaba-sentinel -
在配置文件开启支持:
feign: sentinel: enabled: true
-
实现
fallback
或fallbackFactory
来提供熔断降级逻辑。 -
Sentinel 控制台可用于实时观察流量与熔断策略。
📋 调用过程总览
应用启动
↓
FeignClient 代理创建(动态代理)
↓
封装为 HTTP 请求(RestTemplate/WebClient)
↓
由 LoadBalancerClient 选择服务实例
↓
发起请求 → 服务响应
↓
异常? → Sentinel 接管熔断 → fallbackFactory 执行
👉 延伸阅读:
☁ Nacos vs. Eureka:服务注册与发现对比
🧩 1. 核心功能对比
特性 | Nacos | Eureka |
---|---|---|
服务注册与发现 | ✅ 支持 | ✅ 支持 |
配置中心 | ✅ 内置,支持动态配置 | ❌ 需要借助 Spring Cloud Config |
健康检查 | ✅ 支持 TCP/HTTP/MySQL | ✅ 支持(基于心跳机制) |
注册信息一致性 | 支持 AP 和 CP 模式选择 | AP(最终一致性) |
动态DNS | ✅ 内建 DNS-Based 服务发现 | ❌ 不支持 |
UI 控制台 | ✅ 直观强大 | ✅ 简洁,功能有限 |
社区 & 维护 | 阿里开源,活跃维护中 | Netflix 已停止维护(2.x 后弃用) |
支持协议 | HTTP/gRPC、Dubbo、REST | HTTP(Spring Cloud 封装) |
🔍 2. 原理上的核心区别
✅ Eureka 原理:
- 采用 AP(Availability + Partition tolerance) 模型;
- 注册中心不强制数据一致性,依赖服务端与客户端之间的心跳机制;
- 客户端缓存注册表,即使注册中心短暂不可用,也能完成服务调用(容错性强);
- Spring Cloud 封装使用最广。
✅ Nacos 原理:
- 支持 AP 和 CP 两种一致性协议模式(默认 AP,选项可切);
- 使用 raft/CP 模式下配置和服务数据会强一致;
- 集成配置中心功能,一站式解决注册与配置问题;
- 支持多协议(如 gRPC、Dubbo、HTTP)服务注册发现;
- 提供 命名空间、分组、权重、标签等更丰富的管理功能。
👉 延伸阅读:Nacos
🧪 3. 实践中的选型建议
场景/需求 | 推荐选型 |
---|---|
项目小型,Spring Cloud 全家桶 | Eureka(快速轻便) |
配置中心 + 注册中心统一管理 | Nacos |
使用 Dubbo 或多语言通信场景 | Nacos |
需要强一致性(如金融) | Nacos(CP 模式) |
要求极高的高可用性容错 | Eureka(AP 容忍断连) |
想要统一注册 + 配置 +动态权重等功能 | Nacos |
不再使用被弃用组件 | 推荐 Nacos(Eureka 已停止维护) |
数据库
关系数据库
Mysql 一图流
图中Undo Log不会持久化的说法是错误的
我们知道undo 是作为MVCC实现必不可缺少的一部分,在RR,RC级别下要实现MVCC必须使用Undo Log记录回滚数据链,根据这篇文章RR级别下的幻读问题我们知道Mysql修改数据是原始数据行上修改,利用Undo Log来回滚数据的,所以Undo Log有必要持久化才能防止出现问题。
- 事务崩溃恢复需要它
如果事务执行到一半崩溃(比如断电),在事务还没提交的情况下,MySQL 重启时需要 undo log 来回滚这些未完成的事务。 -
MVCC 的一致性读需要它
一个事务在执行时,可能需要访问旧版本的行(历史版本链)。这些历史版本就存储在 undo log 中(准确说是在 undo tablespace 中的回滚段)。
如果 undo log 不落盘,长事务或系统崩溃后,历史版本就丢了,MVCC 就无法工作。
非关系型数据库
微服务技术相关
基础类库架构
HTTPS
SpringCloud
链路追踪实现原理
熔断与降级
Nginx
虚拟化&容器&调度
Docker
K8S
拓展阅读
netty内存管理
拓展问题
我的一个软件发起了一次http请求,这个请求最终收到了返回,但是被调用的服务被执行了三次,我不小心被调用服务没有做幂等,但是我的调用服务日志显示只调用了一次,我想问如果我使用的是httpclient调用,它在调用内核方法发起网络请求超时会自己重试么
单靠应用层日志无法解释多次调用的问题
你的应用可能只发起了一次:
logger.info("准备发送请求");
httpClient.execute(request); // 你以为只发了一次
logger.info("请求结束");
但:
- 请求发出去了,响应没收到;
HttpClient
底层判断异常(如超时),再发;-
你应用层日志“看不见”第二次发请求的动作,除非你显式开启了底层日志或抓包。
必须结合内核/网络层/传输层等才能定位根因
你的请求路径可能经历了这些阶段:
HttpClient → Java Socket API → OS TCP Stack → 网卡驱动 → 网线 → 路由 → 服务端 → 应用 → 响应返回
可能出问题的地方很多:
层级 | 可能现象 | 对你不可见 |
---|---|---|
Java Socket | connect 成功但写入后超时 | 你以为没发 |
OS TCP | 请求包已发,响应丢失 | 你没收到 response |
网络链路 | 丢包、抖动、代理层 timeout 重放 | 服务被多次触发 |
服务端 | 没做幂等,重复执行业务逻辑 | 非预期副作用 |
客户端 | 自动重试,服务多次执行 | 应用以为只一次 |
如果是偶发现象,很大概率不是你的应用代码直接引起的
这是关键:
- 只出现 极低概率、多数时候正常 的情况;
- 多半是某层发生了 “可恢复性错误”或“轻微的不一致”;
-
导致了 下游服务副作用执行多次;
也就是说,你的应用代码“逻辑上”没问题,但没有做好 网络异常下的防御设计(幂等性 + retry 控制),才让这种偶发问题造成真实伤害。
你该怎么应对这种问题?
措施 | 描述 |
---|---|
✅ 服务端做幂等性设计 | 如插入前校验是否已存在、幂等Token、防重复提交 |
✅ 客户端关闭 POST 重试机制 | 或者只重试 GET |
✅ 所有调用加 traceId(全链路日志) | 用于定位哪一次请求出问题 |
✅ 使用抓包 + 网络日志工具 | 如 Wireshark、tcpdump、-Djavax.net.debug=all |
✅ 使用熔断 + 重试机制而非隐式 retry | 比如 Hystrix / Resilience4j 等有“请求隔离”的中间件 |
✅ 配置合理 timeout | 避免超时过早导致不必要的重试 |
Mysql和Pgsql在隔离级别是RR的时候策略是不同的,或者说它们本来实现MVCC的逻辑就不一样,Mysql使用的undo log,Pgsql使用的元组(我不知道我说的对不对),当Mysql做修改的时候修改的原始数据,根据undo Log来实现事务,而Pgsql是新增元组来实现的,所以Pgsql允许多个修改同时进行(不知道这里对不对),而Mysql一个修改操作会阻塞其他所有的修改操作。我这些理解正确么?
✅ 一、MVCC 的基本差异
项目 | MySQL (InnoDB) | PostgreSQL |
---|---|---|
MVCC 实现方式 | 通过 Undo Log 回滚段(undo segment) | 通过元组版本(Tuple Versioning) |
版本存储位置 | Undo Log + 数据页(数据页中仅存当前版本) | 每次更新生成新的 Tuple,旧版本也保留在表中 |
事务视图(snapshot) | 基于当前事务启动时的 Undo Log 生成 | 基于当前事务的 TxID Snapshot |
✅ 二、你说的几个重点解释
1. 「MySQL 使用 undo log,PostgreSQL 使用元组」
✅ 完全正确。
- MySQL/InnoDB:每条记录只有一个当前版本,旧版本通过 undo log 存在于 rollback segment 中,读取时从 undo log 重建旧版本。
- PostgreSQL:更新/删除会产生新版本(tuple),老版本仍存在(直到被 vacuum 清理),每个版本都有 xmin/xmax 来标示其生命周期。
2. 「MySQL 修改是修改原始数据,PgSQL 是新增元组」
✅ 也是正确的说法,但再具体一点:
- MySQL(InnoDB):
- 对一条记录进行更新时,是在原地修改当前记录,并生成对应的 undo log,用于快照读取和回滚。
- 所以,写操作需要先拿到锁,防止写写冲突。
- PostgreSQL:
- 更新时是插入一条新记录(新 tuple),旧的 tuple 保留,并打上 xmax 表示它被谁删除。
- 这样就避免了多个事务读取/写入同一条记录的直接冲突(MVCC 下读写不阻塞,写写冲突才处理)。
3. 「PgSQL 允许多个修改同时进行,MySQL 会阻塞所有修改」
✅ 你的结论部分正确但需要澄清。
####### ✅ PostgreSQL 的写操作并不是完全“并发”地都能成功:
- PostgreSQL 允许多个事务尝试并发更新同一行数据,但最终只有一个事务能成功,其它会 检测到冲突并回滚 或进入重试(通过序列化器或 application 逻辑)。
- 所以虽然它看起来允许“多个修改同时进行”,但 最终只有一个生效,其它被检测到冲突(如 Tuple 已更新)时会报错或失败。
####### ❌ MySQL 的阻塞也有前提:
- 在 RR(Repeatable Read) 隔离级别下,MySQL 的更新操作默认是获取行级锁,一个事务修改某行时,会阻塞其它想要修改该行的事务,直到事务提交或回滚。
- 所以你说的“阻塞所有的修改操作”是指对同一行,这是对的。
- 但不是全表的阻塞,也不是所有写都会互相阻塞(对不同的行是可以并发写的)。
✅ 三、事务隔离级别:RR 的区别
特性 | MySQL (InnoDB) | PostgreSQL |
---|---|---|
默认隔离级别 | Repeatable Read | Read Committed |
在 RR 下是否避免幻读 | ✅ 避免幻读(gap lock) | ❌ 不能完全避免,需要 Serializable |
可重复读视图实现 | 基于 undo log 的 consistent snapshot | 基于 xmin/xmax 的 snapshot |
✅ 小结
你说的概念整体是非常准确的,细化下来结论如下:
结论 | 是否正确 | 说明 |
---|---|---|
MySQL 使用 undo log 实现 MVCC | ✅ | 正确 |
PostgreSQL 使用元组版本实现 MVCC | ✅ | 正确 |
MySQL 的修改是修改原数据,PgSQL 是新增元组 | ✅ | 正确 |
PostgreSQL 允许多个并发修改操作 | ✅但需注意冲突检测 | 多个事务可以尝试修改,但不能都成功 |
MySQL 一个修改会阻塞其它所有修改 | ✅但仅限于同一行 | 同一行上的行锁,阻塞其它写事务 |
✅ 延伸思考
- PostgreSQL 的 MVCC 实现虽然支持高并发和非阻塞读写,但需要 VACUUM 机制 定期清理无效版本,否则表膨胀。
- MySQL 的 undo log 基于回滚段,控制更集中,但 undo log 的使用和清理(purge)策略也会影响性能。
Mysql查询学科前三名
在 MySQL 中,如果你有一个学生成绩表,想要按学科查询每个学科的前三名成绩,常用的解决方案是使用 窗口函数,如 ROW_NUMBER()
、RANK()
或 DENSE_RANK()
(MySQL 8.0 及以上支持)。
✅ 假设你的表结构如下:
成绩表: scores
+----+--------+----------+--------+
| id | name | subject | score |
+----+--------+----------+--------+
| 1 | 张三 | 数学 | 98 |
| 2 | 李四 | 数学 | 89 |
| 3 | 王五 | 数学 | 92 |
| 4 | 赵六 | 数学 | 76 |
| 5 | 张三 | 英语 | 88 |
| 6 | 李四 | 英语 | 90 |
| 7 | 王五 | 英语 | 86 |
| 8 | 赵六 | 英语 | 91 |
+----+--------+----------+--------+
📌 查询每个学科的前三名(按分数从高到低):
SELECT id, name, subject, score
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC) as rn
FROM scores
) AS ranked
WHERE rn <= 3;
🔍 说明:
ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC)
:- 将数据按
subject
分组,并在每组中按score
降序排序。 - 给每行数据一个序号
rn
。
- 将数据按
- 最外层查询筛选出
rn <= 3
的,即每个学科的前三名。
📝 如果有并列名次怎么办?
可以使用 RANK()
或 DENSE_RANK()
替换 ROW_NUMBER()
:
RANK()
:并列名次会跳过名次(比如两个第一名,下一个是第三名)DENSE_RANK()
:并列名次不会跳过(两个第一名,下一个是第二名)
🧠 如果你用的是 MySQL 5.7 或以下(不支持窗口函数),可以用 相关子查询 方式实现:
SELECT s1.*
FROM scores s1
WHERE (
SELECT COUNT(*)
FROM scores s2
WHERE s2.subject = s1.subject
AND s2.score > s1.score
) < 3;
📌 说明(旧版语法逻辑):
- 对每条记录
s1
,用一个子查询s2
统计比它成绩高、同学科的记录数; - 若这个数量
< 3
,说明它是该学科的前3名。
📝 注意事项:
- 这种方式在成绩并列时可能返回多于3条(即出现“并列第三”情况);
- 如果你希望严格只取3个名额(不管并列),请使用
ROW_NUMBER()
; - 如果你希望处理“并列”,用
RANK()
或DENSE_RANK()
替代ROW_NUMBER()
。
什么是闭包
闭包(Closure)是编程语言中的一个重要概念,尤其在 javaScript、Python 等支持一等函数(First-class Function)的语言中非常常见。它可以简洁地定义为:
闭包是函数与其定义时所处的词法作用域(Lexical Environment)的绑定组合,即使这个作用域在函数执行时已经超出了其作用范围,函数仍然可以访问这个作用域中的变量。
一、为什么需要闭包?
闭包主要用于:
- 函数内部保存“外部”变量的状态(数据持久化)
- 实现封装与数据私有化
- 避免全局变量污染
- 函数式编程中的高阶函数应用(如柯里化)
二、javaScript 中闭包示例(经典)
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer(); // outer执行后返回inner函数,并绑定了其作用域
counter(); // 输出 1
counter(); // 输出 2
解释:
outer
执行后返回了inner
函数;inner
函数引用了outer
中的count
变量;- 即使
outer
已经执行完返回了,inner
依然可以访问count
,因为闭包保持了 outer 的词法作用域。
三、闭包的本质(深入理解)
- 函数是一等对象:可以作为参数传递或作为返回值返回;
- 词法作用域(Lexical Scope):函数定义的位置决定了它能访问哪些变量;
- 闭包持有引用:闭包不是拷贝了变量,而是持有对变量的引用;
- 内存泄漏风险:闭包可能会导致某些变量长期驻留在内存中。
四、Python 中的闭包(例子)
def outer():
count = 0
def inner():
nonlocal count
count += 1
print(count)
return inner
counter = outer()
counter() # 输出 1
counter() # 输出 2
nonlocal
表示使用外部非全局作用域中的变量。- 同样,
inner
闭包“记住了”count
。
五、判断是否形成了闭包?
在 javaScript 中:
- 函数 A 内部定义并返回了函数 B;
- B 使用了 A 中的局部变量;
- 即使 A 执行完毕,B 仍然能使用这些变量。
👉 满足上述条件就形成了闭包。
六、使用闭包实现数据私有化
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.decrement()); // 0
count
变量对外不可见,形成了私有变量。- 这种用法是模块化编程常见技巧。
七、闭包的弊端与注意点
- 容易导致 内存泄漏(引用未释放)
- 不小心会造成 变量共享问题
- 不能滥用闭包,否则代码可读性和调试性下降
总结一句话:
闭包是函数+定义时作用域的组合,使函数可以“记住”并访问它定义时的变量,即使这些变量在其词法作用域之外。
hotspot虚拟机不是基于寄存器的架构,那么读取数据的时候是否会有延迟
🌟 问题背景简述
HotSpot 虚拟机采用的是 基于栈的架构(stack-based VM),而非寄存器架构(register-based VM)。这意味着大多数指令都以操作数栈作为操作对象,而不是寄存器。
✅ 栈架构 vs 寄存器架构:读取数据效率比较
栈架构(如 HotSpot):
- 每条指令操作栈顶元素,如
iload_0
表示将局部变量表中第0个整型值压栈。 - 优点:
- 指令集更紧凑,指令长度更短(因为无需显式指定寄存器)。
- 更容易跨平台(java 设计初衷)。
- 缺点:
- 操作需要频繁地进行压栈、出栈,中间结果频繁进出栈,可能引入额外开销。
- 理论上,执行同一逻辑需要更多的指令次数。
寄存器架构(如 Dalvik、LLVM IR):
- 每条指令直接操作寄存器,如
ADD r0, r1, r2
。 - 优点:
- 更少的内存访问,中间结果保存在寄存器中(如果寄存器够用)。
- 性能通常优于栈架构。
- 缺点:
- 指令更复杂,指令长度更长。
- 不容易跨平台(硬件相关性强)。
❓HotSpot 使用栈架构会导致访问延迟吗?
从字节码层面看:确实存在更多的数据操作指令,中间值需要频繁入栈/出栈。
但:
✅ 真正运行时,HotSpot 并不是直接解释字节码,而是会进行一系列优化:
🚀 HotSpot 的运行时优化
1. 解释器(Interpreter)阶段:
- 初始运行时确实采用栈架构解释执行。
- 所以在解释模式下,频繁入栈/出栈可能带来一定开销和延迟。
2. 即时编译(JIT)阶段:如 C1/C2 编译器
HotSpot 会将热点代码编译为本地机器码,且:
🔥 在 JIT 编译后,HotSpot 会将栈式字节码转换为寄存器分配的本地代码!
也就是说:
- 编译后的代码在运行时已经 不再使用操作数栈,而是尽可能使用物理寄存器。
- 并且使用了复杂的寄存器分配算法(如线性扫描或图着色)。
- 此时的访问效率是高度优化的,延迟远低于解释模式。
🧠 总结回答你的问题:
HotSpot 虚拟机不是基于寄存器的架构,那么读取数据的时候是否会有延迟?
答:
- 在解释执行阶段,确实由于基于栈架构,需要频繁压栈出栈,会有一定的指令层面延迟,不如寄存器架构高效;
- 但在JIT编译后(尤其是C2编译器输出的代码),HotSpot会将栈架构优化为寄存器分配的机器码,运行效率极高,读取数据非常快,延迟问题基本不存在。
volatile 在java中起到lock的作用,就是读取对应内存值的时候不读取寄存器的值,根据mesi协议,如果遇到这个内存的值,是不是一定读取内存的值,而不是直接读取1,2级缓存的值
✅ 一、volatile
是不是等价于 lock?
不是。
特性 | volatile |
synchronized / lock |
---|---|---|
可见性 | ✅ 有 | ✅ 有 |
有序性(禁止指令重排) | ✅ 有(只针对该变量) | ✅ 更强(全域) |
原子性 | ❌ 没有 | ✅ 有 |
对应机器指令 | lock 前缀指令(如 lock cmpxchg ) |
lock 伪指令+monitor |
volatile
只保证对该变量的 读写是有序且立即可见的,但不保证原子性。
🧠 二、volatile
的作用是啥?
java 编译器和 JVM 在遇到 volatile
变量时,会生成特殊的汇编指令来保证 内存可见性 和 禁止指令重排序:
- 写操作后,会插入
store-store
、store-load
内存屏障(Memory Barrier); - 读操作前,会插入
load-load
、load-store
屏障。
而这些指令,通常在底层 x86 CPU 上使用类似于:
lock xchg
或者是强制使用 MFENCE
/LFENCE
/SFENCE
指令。
🔄 三、MESI 协议和缓存一致性
✅ 什么是 MESI?
CPU 的缓存一致性协议,四种状态:
状态 | 名称 | 描述 |
---|---|---|
M | Modified | 缓存为修改态,值未写回内存,独占 |
E | Exclusive | 缓存为独占态,值与内存一致,只有自己有 |
S | Shared | 缓存为共享态,与其他核心共享,未修改 |
I | Invalid | 缓存为无效态,不可读 |
🎯 你的核心问题:
当有
volatile
修饰变量时,如果某个线程读取这个变量,是不是一定会从主内存读取,而不会使用 L1/L2 缓存?
❌ 答案是否定的 —— 不是一定读主内存。
✅ 真相:会读缓存(但要确保是最新的)
volatile
并不会让每次访问都跳过缓存直接去主内存。
那样太慢,根本无法接受。
正确做法是:
- 通过内存屏障 + MESI 协议,确保你读取的缓存内容是「最新的」。
####### 🧵 举个真实场景:
线程 A 写了 volatile x = 1
,线程 B 读它。
- A 写的时候触发:
lock
指令或store-load barrier
;- 让该缓存行进入 M 状态,并广播 Invalidate 到其他核心,强制 B 的缓存行变为 I 状态。
- B 读的时候:
- 发现自己缓存中 x 的行是 I 状态(Invalid);
- 触发从其他核心或主内存中拉取最新数据;
- 拉取后放入自己的缓存行,可能以 S 状态或 E 状态存储。
关键点:
B 最终读取的数据是最新的,但它最终仍然是存在 L1/L2 缓存中的,只是这个数据是有效且可见的。
✅ 总结你的问题答案:
问题 | 回答 |
---|---|
volatile 是否起到 lock 的作用? |
❌ 不起到原子性的 lock 作用,只保证可见性和有序性 |
使用 volatile 变量时是否会跳过缓存? | ❌ 不会直接跳过,而是通过缓存一致性机制使缓存行最新 |
是否每次都从主内存读取? | ❌ 否,读取仍是从缓存,但缓存必须是最新有效的(非 Invalid) |
volatile 如何实现内存可见性? | ✅ 通过内存屏障 + MESI 协议使缓存失效或同步 |
📌 补充小结(一图概括)
java volatile -> 内存屏障 + 禁止重排序
↓
CPU 执行 lock 或 fence 指令
↓
使其他核心的缓存行失效(MESI协议)
↓
其他线程读取时重新拉取最新值(缓存or内存)
↓
✅ 保证可见性,不等于总是读主内存