2025年暑期学习日记
Java-Drill
2025.07.01
1.标识符和关键字的区别是什么?
标识符简单来说就是指给程序、类、变量、方法等取的名字
关键字指的是被赋予特殊含义的标识符,比如访问控制符,程序控制符、错误处理、类和方法和变量的修饰符等
2.四大访问控制符
访问权限由小到高依次如下;
private: 仅在当前类中可见。可修饰对象:类、变量、方法。 注意:不能修饰外部类
default: 在同一包内可见。默认,不使用任何修饰符,可修饰对象:类、变量、方法、接口
protected: 在同一包内的类和所有子类可见。可修饰对象:变量、方法。 注意:不能修饰外部类
public: 对所有类可见。使用对象:类、变量、方法、接口
提问:
外部类为什么不能使用protected、private修饰?
3.自增自减运算符
4.移位运算符
java里数值类型存储形式是补码形式进行存储的。
<<:左移,低位补零,当移动位数大于等于当前变量的二进制最大表示位数时,进行取余再移位运算。所以极限情况也就是最低位移到最高位,剩余位全为0
>>算术右移,带符号右移
>>>:逻辑右移,无符号右移
5.continue、break 和 return 的区别是什么?
continue:跳出当前循环,进行下一轮循环
break:跳出当前循环体,执行循环体后面的语句
return:结束当前方法执行,如果方法不是被void修饰,则返回指定类型的值
6.java的基本数据类型
4种整数类型:byte(8位)、short(16位)、int(32位)、long(64位) ;均为有符号整数
2种浮点型:float(32位)、double(64位)
1种字符型: char(无符号16位)
1种布尔型: boolean(1位)
注意:
1.Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
2.Java 里使用 float 类型的数据一定要在数值后面加上 f 或 F,否则将无法通过编译。
3.char a = ‘h’:单引号,String a = “hello” :双引号。
7.基本类型和包装类型的区别?
根据5点分析:用途、存储方式、默认值、比较方式、占用空间
8.包装类型的缓存机制
9.自动装箱与拆箱了解吗?原理是什么?
2025.07.02
今日学习内容:
1.字节码文件的组成
基本信息、字段、常量池、方法、接口等
2.字节码文件的查看方式
本地文件和开发环境用jclasslib工具、服务器上文件用javap命令、线上监控用Arsath(dashboard、dump、jad等命令
3.类的生命周期分为5个阶段
加载、连接、初始化、使用、卸载,其中连接又可以细分为3个小阶段(验证、准备、解析)
tip
| 问题(一个项目启动时) | 答案 |
|---|---|
| 所有类都会加载吗? | 用到(6种情况)才加载,不是全部(懒加载) |
| 加载了就会初始化吗? | 不会,被动引用(子类对象引用父类的静态变量时,子类只会加载和连接,父类会加载、连接和初始化)场景只加载不初始化 |
| 连接阶段呢? | 加载完成后连接就开始了,但解析阶段可以延迟 |
触发加载的”用到”情况
- 创建实例
1 | new Foo(); // 必须加载 Foo,因为要分配内存、知道字段布局 |
- 访问静态成员(变量或方法)
1 | Foo.staticVar; // 加载 Foo |
- 使用类的静态常量(注意:编译期常量除外)
1 | Foo.CONST; // 如果 CONST 是 static final int = 1(编译期常量),可能不加载 |
- 反射调用
1 | Class.forName("Foo"); // 明确加载 |
- 子类初始化触发父类加载
1 | new Sub(); // 先加载 Super,再加载 Sub |
- 包含 main() 的启动类
1 | java com.example.Main // 首先加载 Main 类 |
4.类加载阶段

简单来说就是把字节码信息通过不同渠道(本地文件、动态代理生成、通过网络传输的类)以二进制流方式加载到内存的方法区(为InstanceKlass对象)中 。
– 注意字节码信息(即InstanceKlass对象)存放在方法区、而字节码信息关联的Class对象存放在堆上,它们之间是相互关联的。静态字段信息在jdk8之前存放在方法区中的InstanceKlass对象,jdk8及以后存放在堆中Class对象上
– 此外注意Class对象产生原因:1.字节码信息是通过c++编写的对象,使用java代码无法直接操作。Class对象是经java包装后的对象,方便访问。 2.InstanceKlass对象包含了部分开发者用不到的信息(例如虚方法表),Class对象剔除了跟开发无关的信息,控制开发者访问数据的范围,以提升数据的安全性
学习工具:Hsdb
在java文件的bin目录下使用jhsdb hsdb命令打开调试窗口。
5.类连接阶段
– 验证: 检测字节码文件信息是否符合java虚拟机规范(例如文件开头的魔数是否是cacfbabe、指令语义、主副版本号)
– 准备: 给静态成员变量分配内存并赋初始默认值,如果静态变量还由final修饰,则直接在该阶段赋指定值。原因:在编译阶段就把final修饰的变量进行了赋值,jvm看到字节码文件的变量已经有值了,就直接在内存中赋值了,而仅由static修饰的变量则会先赋默认值。

– 解析: 将常量池的符号引用替换为指向内存的直接引用
符号引用指的是通过编号得到的内容不是所需数据(即编号不是直接地址),而是数据的标识符和数据的直接地址。我的理解就是类似于计组中的间接寻址。

直接引用指的是不再使用编号(索引)间接访问,而是直接通过内存地址访问具体的数据。
如下所示,在运行进程中可以看到当前类的Super Class已经直接指向了类的具体地址,而不是其索引

2025.07.03
1.类的初始化阶段
类的初始化阶段就是执行静态代码块并为静态变量的赋值。(只会执行一次)
主要就是执行字节码文件中clinit(全称class init)部分的字节码指令,如下图所示

注意,因为a被final修饰,且等号右边是常量,所以在类的连接阶段的准备阶段就已经赋值了,所以无需在clinit字节码指令中再次赋值。
导致类初始化的四种情况:
- 访问一个类的静态变量或静态方法,注意变量是final修饰且等号右边是常量不会触发初始化。
- 调用Class.forName(String className)
- new一个该类对象时
- 执行Main方法的当前类
测试代码:
1 | public class HsdbDemo { |
有意思的案例:
1.静态代码块放在在静态变量之前

主要还是因为跟类的生命周期相关,i在连接阶段被赋予了初值,而静态代码块在初始化阶段才执行,所以并不会说执行时报没有i这个变量的错误。从字节码的clinit的字节码指令也可以看到,先是赋值了111,再赋值了0给i。所以clinit方法的字节码指令顺序和代码编写的顺序一致。
注意,这里主要还是编译器的支持:Java 允许静态代码块中对后面声明的静态变量赋值,只要不是读取。如果改成 System.out.println(i); 就会编译报错(出于规范考虑,这是非法前向引用,虽然不能通过编译,但理论上,执行是没有任何问题的)

2.new子类时,父类先初始化,子类再初始化,先执行父类代码块和构造函数,再执行子类的。代码块比方法构造函数优先执行。
1 | public class HsdbDemo { |
执行结果
1 | Demo02静态代码块执行了 |

3.创建对象的数组不会进行初始化
1 | public class HsdbDemo { |
因为数组本身也是一个对象,new Demo02[10]实际上是创建数组对象(类型为Demo02[]),并非创建Demo02对象,数组中还未有Demo02对象,默认均为null。
2.类加载器的作用
负责在类加载过程中获取字节码信息并加载到内存这一部分。它主要是把字节码加载进内存并转换为byte[]数组,之后会调用jvm底层方法将byte[]数组转换成方法区和堆中的对象。
3.类加载器的分类
jdk8及以前分为两类,一类是通过java代码实现的(扩展类、应用类加载器),一类是通过jvm底层源码实现的(c++编写,启动类加载器)。
启动类加载器:加载java中最核心的类。默认加载java安装目录/jre/lib下的类文件,如rt.jar等
扩展类加载器:加载java中比较通用的类。默认加载java安装目录/lib/ext目录下的扩展类库
应用类加载器:加载自定义的类和引入的第三方jar包中的类。默认加载用户类路径(classpath)下的类。
自定义类加载器:自定义的类加载器
层级关系:
tip1:当java代码试着获取由启动类加载器加载的类的加载器时,会返回null。(原因:1.由c++编写,所以无法直接获取启动类加载器的信息 2.偏于底层,又位于双亲委派机制的最上层,防止用户对其进行恶意操作,增强系统安全性)
tip2:自定义类加载器父类是启动类加载器的原因是
4.双亲委派机制
原理、作用
原理:当一个类加载器试着去加载类的时候,它会以递归方式向上查看其父类加载器是否加载过这个类,如果最后到了启动类加载器也没有加载过这个类,则会从启动类开始自上往下试着加载这个类。有效避免了类的重复加载,并防止核心类被重写覆盖,提升系统安全性。
作用:1.防止重复加载 2.避免恶意修改核心类库
5.如何打破双亲委派机制
1.自定义类加载器:修改loadClass方法,把双亲委派机制的代码去除即可
需要先了解类加载的相关代码

2.利用线程上下文类加载器
涉及知识点:SPI机制+线程上下文类加载器
JDBC案例为例。
DriverManager是由启动类加载器加载的,而DriverManager的静态代码块中包含了让jar包中的mysql驱动则直接委派给了应用类加载器去加载的方法,故其初始化阶段就会通过直接委派方式加载mysql驱动,没有经过双亲委派机制正常流程。
在JDBC中,DriverManager用于管理不同数据库的驱动。例如,MySQL的驱动和Oracle的驱动。DriverManager类位于rt.jar包中,由启动类加载器加载。然而,用户JAR包中的驱动需要由应用程序类加载器来加载(因为驱动是由外部提供的,也就是说,DriverManager的加载依赖于外部驱动类的加载),这就违反了双亲委派机制。
这个问题的解决方法的实现原理就是利用到了SPI机制,也就是服务提供者发现机制。
在ClassPath路径下的META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件中写该接口的实现:
例如,在MySQL JDBC驱动中,有一个名为java.sql.Driver的文件在META-INF/services文件夹中,文件内容为com.mysql.cj.jdbc.Driver。这样,当DriverManager需要加载MySQL驱动时,它会通过SPI机制,首先查看当前线程的上下文类加载器(通常是应用程序类加载器)是否能加载这个驱动。由于驱动的实现类是由应用程序类加载器加载的,这就打破了双亲委派机制。

获取应用类加载器的简单方式:获取当前线程的上下文加载器即为应用类加载器
1 | public static void main(String[] args) throws ClassNotFoundException { |
注意一点:是SPI机制的核心类ServiceLoader利用到了线程的上下文类加载器去加载驱动。打破双亲委派机制的具体原因在这里。

SPI机制
2025/8/26 补
核心类:ServiceLoader
小总结:SPI机制本质就是提供了一个动态获取某个接口的实现类的方法,因为是动态获取的,所以,并不需要我们在代码中创建接口的具体实现,相反,可以直接引入jar包,在需要时,通过线程上下文类加载器去jar包中动态加载并获取类的实例。
想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,ServiceLoader本质就是通过反射和类加载器来创建实现类的实例。
ServiceLoader的具体实现可以去看javaGuide的这篇文章。
ServiceLoader是懒加载模式,只有在真正去遍历实现类时,才会真正扫描并加载对应的实现类。
工作流程总结
- 初始化阶段:调用ServiceLoader.load()时只是创建了ServiceLoader实例和LazyIterator,没有实际加载
- 首次调用hasNext():
- LazyIterator开始查找META-INF/services/接口全限定名配置文件
- 解析配置文件内容,获取服务实现类名列表
- 调用next():
- 使用Class.forName()加载服务实现类
- 通过newInstance()创建实例
- 缓存实例到providers中
- 后续调用:优先返回已缓存的服务实例
- 这种懒加载机制确保了只有在真正需要使用服务时才会加载和实例化相应的类,提高了系统性能并减少了资源浪费。
线程上下文类加载器默认情况下是应用程序类加载器(Application ClassLoader),它负责加载 classpath 上的类。当核心库需要加载应用程序提供的类时,它可以使用线程上下文类加载器来完成。这样,即使是由引导类加载器加载的核心库代码,也能够加载并使用由应用程序类加载器加载的类。
SPI机制使用小案例
项目结构

各类代码
Main
1 | public class Main { |
LoggerService
1 | public class LoggerService { |
Logger
1 | public interface Logger { |
LogBack
1 | public class LogBack implements Logger { |
LogBack2
1 | public class LogBack2 implements Logger { |
6.JDK9及以后的类加载器
jdk9及以后引入了module的概念,即模块化思想,方便类的扩展。
变化:
1.启动类加载器由java代码编写,但获取时依然为null,保持了和之前类加载器设计的统一,同样也是为了防止用户恶意操作。
2.扩展类加载器被替换为了平台类加载器,遵循模块化方式加载字节码文件,和启动类加载器(也可以扩展)没有什么特殊区别,更多的是为了与老版本设计方案的兼容。
2025.07.04
算法题:
旧:两数之和、两数相加、无重复字符的最长字串
新:Z字形变换、整数反转
八股文:
jvm学习:
1.运行时数据区
指的是jvm在运行java程序时所管理的内存。
按线程共不共享可分为两类:
线程共享:方法区、堆
线程不共享:程序计数器(存放下一条将要执行的字节码指令的内存地址)、java虚拟机栈(存放调用java编写的方法时产生的栈帧)、本地方法栈(存放使用c++编写的,即native修饰的方法产生的的栈帧)
2.栈帧
java虚拟机中采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法调用都会使用一个栈帧来保存。
栈帧主要由三部分组成,局部变量表、操作数栈和帧数据
其中局部变量表和操作数栈可以联想字节码文件中的方法对应的指令,就是用到了局部变量表和操作数栈。
(1)局部变量表:存放方法运行过程中的所有局部变量。
字节码文件中的局部变量表解读:

注意:
1.槽可以复用。例如方法内若包含的代码块,则其执行完成后,后面代码的变量的可以复用代码块中使用的槽。
2.对于实例方法(即非static方法),局部变量表的0号位置存放的是当前实例的引用(即this),这也解释了this从哪里来,原来一开始就存放到局部变量表上了。
3.可以通过debug查看当前虚拟机栈的内容和每个栈帧包含的局部变量情况。

(2)操作数栈:在指令执行过程中存放临时数据
(3)帧数据:主要包含动态链接(包含当前方法运行时的常量池引用)、方法出口(存储方法执行完成后返回的地址)、异常表的引用
2025.07.05
算法题:
旧:字母异位分词、最长连续序列、移动零、盛水最多的容器
新:二叉树中序遍历
八股文:
java基础上、java基础中
jvm学习:
1.栈内存溢出
2.堆内存溢出
3.方法区
方法区主要包含三部分:
类的元信息(即类的基本信息):一般称为InstanceKlass对象,在类的加载阶段创建完成。
运行时常量池(字节码文件中的常量池内容):字节码文件中通过编号查找表的方式找到常量,称为静态常量池。而当常量池加载到内存时,可以通过内存地址快速定位到常量池的内容,即符号引用 -> 直接引用,这种形式的常量池称为运行时常量池。

字符串常量池(字符串常量):JDK6及以前,字符串常量池归属于运行时常量池,一起存放在永久代中,而JDK7时,字符串常量池存放在永久代以外的堆上,JDK7及以后,方法区存在在元空间中,堆上就没有永久代了,而字符串常量池依旧存放在堆上。JDK 7+ 的字符串常量池本质上是一个引用表,真正的字符串对象都在堆中!!!。

此外,String对象的intern()方法可以手动将字符串加载进字符串常量池中。
有意思的代码:
JDK8及以后s1才等于s2,之前是false(直接拷贝字符串到字符串常量池,而不是存放字符串对象的引用)
1 | public static void main(String[] args) { |
方法区的存放位置:在JDK7及以前,方法区是存放在堆当中的,存放区域被称为永久代,故内存上限取决于堆的内存上限。而JDK8以及后,存放在元空间当中,元空间指的是由操作系统直接管理的内存,内存上限取决于操作系统能承受的上限。
两段代码的差异分析
代码一(三个 true)
1 | String s2 = new StringBuilder().append("ni").append("hao").toString();// 堆中new新对象 |
时序:
1 | ① s2 → 堆中new出"nihao" |
代码二(false, true, false)
1 | String s = "nihao"; // 常量池不存在"nihao"的引用,在堆空间创建"nihao"这个字符串对象,然后保存引用到常量池 |
时序:
1 | ① s = "nihao" → 堆种new 出 "nihao"对象,常量池记录引用 |
4.直接内存
即由操作系统直接管理的内存,如元空间就属于直接内存。
2025.07.06
算法题:
新:104二叉树的最大深度、226翻转二叉树、101对称二叉树、543二叉树直径、102二叉树的层序遍历
八股文:
java基础中
jvm学习:
1.自动垃圾回收——方法区的回收
方法区回收的主要是不再使用的类。
需要同时满足以下三个条件
1.此类的所有实例对象已经被回收,即堆中不存在该类的实例对象及子类对象
2.加载该类的类加载器也已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用
2.自动垃圾回收——堆的回收
在java中,堆中对象是否能被回收主要看其是否被引用来决定。
至于如何判断对象是否被引用,常见的有2种方式:引用计数法和可达性分析法。因为引用计数法无法解决堆中对象相互引用时无法回收的情况,此外维护计数器也会对性能产生影响,所以java使用的是可达性分析算法。
可达性分析法
可达性分析法将对象分为两类,一类是垃圾回收的根对象(GC Root),一类是普通对象,对象之间存在引用关系。
如果某个对象通过GC Root是可达的,那么这个对象就无法被回收。

可称为GC Root对象的四大类:
- 线程Thread对象
- 系统类加载器加载的java.lang.Class对象
- 监视器对象(保存了被synchronized关键字持有的对象)
- 本地方法调用时使用的全局对象
java中的5中对象引用方式
上面的可达性分析法描述的对象引用,一般指的是强引用,即只要对象通过GC Root是可达的,那么对象就一定不能被回收。
软引用:如果一个对象只有软引用对象关联到它,那么当内存不足时,就会回收该对象。至于软引用对象本身的回收,则需要通过引用队列进行回收,即当软引用对象关联的对象被回收时,就会把自己放入到引用队列中,通过代码遍历队列,将软引用对象(SoftReference)的强引用删除。
弱引用:当一个对象只有弱引用对象关联到它时,是无论内存是否充足,均会对该对象直接进行回收。
虚引用(幽灵引用/幻影引用):不能通过虚引用获取到关联的对象,虚引用的作用只是在关联的对象被回收时可以接收到对应的通知。就比如直接内存的回收就使用到了虚引用,即当直接内存对象被回收时,可以接收到通知,然后把这个对象用到的直接内存进行回收。
终结器引用:主要就是对象被回收时,如果重写了finalize()方法将自身被强引用关联,则可以复活,当第二次进行回收时才会被真正回收。
终结器引用是 JVM 用于实现 finalize() 的内部机制,允许对象在被回收前执行一次清理逻辑,但不建议依赖它来“复活”对象,更不建议作为资源释放的主要手段。
| 用途 | 说明 |
|---|---|
| 资源清理的最后兜底机制 | 比如关闭文件、释放 socket、清理 JNI 句柄等 |
| 不适合做主要资源释放方式 | 因为调用时机不确定,容易造成资源泄漏 |
| 已被官方废弃/不推荐使用 | Java 9 起,finalize() 被标记为 deprecated,推荐使用 try-with-resources 或 Cleaner |
2025.07.07
算法题:
无,明天一定┭┮﹏┭┮
八股文:
java基础下
jvm学习:
1.垃圾回收(GC)算法
之前学习了如何判断对象能否被回收(可达性分析法),而至于对象是怎么被回收,就是今天所学的内容了。
核心思想:
- 找到内存中存活的对象。
- 释放不再存活对象的内存,使得程序可以再次利用这部分内存。
垃圾回收算法的评价标准:
Java垃圾回收过程会通过单独的GC线程来完成,但不管使用哪种GC算法,都会有部分阶段需要停止所有的用户线程,这个过程称为STW(stop the world)。STW测试代码:jvm/day07/StopTheWorldTest
主要从三方面考虑:
1.吞吐量:指CPU用于执行用户代码的时间与CPU总执行时间的比值。即吞吐量=执行用户代码时间/(执行用户代码时间+GC时间),吞吐量越高,垃圾回收的效率越高。
2.最大暂停时间:指所有垃圾回收过程中的STW时间最大值。最大暂停时间越短,用户使用系统时受到的影响就越小。
3.堆的使用效率:不同垃圾回收算法对堆内存的使用是不同的,比如标记清除算法,可以使用完整的堆内存,而复制算法则会将堆内存一分为二,每次只能使用一半内存。从使用效率来说,标记清除算法是由于复制算法的。
上述三种标准一般不可兼得。
一般来说,堆内存越大,最大暂停时间越长(因为要回收的垃圾多了),想要减少最大暂停时间又需要多次暂停(每次暂停都需要一定的开销时间),就会降低吞吐量。
分类:
标记-清除算法
分两个阶段:
(1)标记阶段,将所有存活对象进行标记。java使用可达性分析法,从GC Root开始通过引用链遍历出所有的存活对象。(不同的垃圾回收器,它的存活标记存放位置是不同的)
(2)清除阶段:从内存中删除没有被标记的非存活对象。
优点:实现简单,只需维护标志位。
缺点:1.碎片化问题 (类似操作系统中的外部碎片)。 2.分配速度慢,因为内存碎片存在,所以需要维护一个空闲链表来供新对象找寻存放位置 (还是操作系统的知识)。

复制算法
复制算法主要是将堆内存分为两块空间,To空间和From空间。一开始对象都存放在From空间,GC阶段开始时,将From空间的GC Root和其关联的对象搬运到To空间,之后清空From空间,并把两空间名称进行互换,以便下一次GC。
优点:不会发生碎片化问题(移动到To空间时,会按对象顺序连续存放)
缺点:内存使用效率低(仅一半的空间可以用来存储对象)
标记-整理算法
是对标记清除算法容易产生内部碎片问题的一种解决方案。
(1)标记阶段
(2)整理阶段,将存活对象移动到堆的一端,清理掉非存活对象的内存空间。
优点:内存使用效率高、不会发生碎片化问题
缺点:整理阶段效率不高
分代GC
目前应用最广的分代GC,是将上述算法组合(年轻代复制算法,而老年代看使用的垃圾回收器决定)进行使用。
分代GC将整个内存区域划分为了年轻代和老年代。
年轻代:年轻代被划分为了伊甸园区(Eden)和幸存区(划分为了From区和To区)。使用复制算法。
老年代:存放年龄到达阈值的对象,即存活比较久的对象。注意To区溢出的年轻代对象也会转移至老年代,即使年龄未打到阈值。使用标记整理算法。
垃圾回收流程:测试代码jvm/day07/GCDemo01
首先,新创建的对象(小对象,大的直接放到老年代区)都会放入到年轻代的Eden区。当Eden区满了,就会触发年轻代的GC,称为Minor GC。Minor GC会把eden和From中不需要的回收的对象放入到To区。然后清空eden区和From区。清楚完之后,会把From区和To区进行名称互换。对象每次在幸存区的转移都会让它的年龄加1,当年龄到达阈值时,就会转移到老年代中。当老年代内存满了的时候(不同的垃圾回收器,机制有所不同),就会先进行Minor GC(主要是试着回收To区溢出到老年代的对象),如果空间还是不足,会先进行Major GC(老年代的垃圾回收),如果空间还是不足,则再进行Full GC,Full GC会对整个堆进行垃圾回收,如果Full GC后内存还是不足,则会抛出Out of Memeory异常。
分代回收的核心思想就是尽可能做Minor GC 而不做 Full GC,使得STW尽可能短。
回收器组合 触发老年代回收的时机 “空间不足”时的首要反应 是否会触发长时间STW Full GC? 关键风险 Parallel Scavenge/Old 晋升失败时 直接触发Full GC 是,必然步骤 停顿时间长 ParNew + CMS 占用率阈值 + 分配担保预测 尝试并发收集 (CMS) 可能,在“并发模式失败”或碎片时 并发失败、碎片化 G1 基于停顿预测模型 尝试Mixed GC 可能,在回收跟不上分配时退化 退化到Serial Old ZGC/Shenandoah 基于分配/时间 加速或调整并发周期 几乎不会 并发CPU开销大
思考:
为什么分代GC算法要把堆分成年轻代和老年代?
答:因为系统中的绝大部分对象都是创建完后很快就不再使用了,比如用户获取的订单数据,订单数据返回给用户后就可以释放了。而老年代存放的是长期存活的对象,比如Spring中的大部分Bean对象,在程序启动之后就不会被回收了。因为新生代的对象很快就会被清除,而老年代对象会长期存活,故在虚拟机默认设置中,新生代大小要远小于老年代。
关于major gc是否存在的场景:
- Serial Old / Parallel Old
- 没有独立的Major GC概念,老年代满了直接触发Full GC
- 回收时会连带年轻代一起清理
- CMS(Concurrent Mark Sweep) ⭐ 典型代表
1 | 年轻代:ParNew GC(Minor GC) |
- CMS就是专门的老年代收集器,可以与年轻代GC独立运行
- 老年代不足时触发CMS,不会动年轻代
- 只有当CMS失败或并发模式失败时,才退化为Full GC(Serial Old)
- G1(Garbage First)
1 | Mixed GC:同时回收年轻代 + 部分老年代Region |
- 没有纯粹的Major GC,但Mixed GC会包含老年代Region
- 老年代占比达到阈值(默认45%)触发并发标记,之后做Mixed GC
- ZGC / Shenandoah(低延迟收集器)
- 不分代或弱分代,没有传统意义上的Minor/Major/Full GC区分
- 以Region为单位并发回收
2.垃圾回收器
垃圾回收器是垃圾回收算法的具体实现。
分组:
1.Serial和Serial Old:
Serial和Serial Old都是采用单线程串行进行垃圾回收。其中,Serial负责新生代,使用复制算法。而Serial Old负责老年代,使用标记整理算法。它们优点就是单cpu情况下吞吐量出色,但在多核cpu下,性能不如其他多线程回收的垃圾回收器,此外,当堆空间偏大时,STW时间也会比较长,影响用户体验。

2.ParNew和CMS
ParNew使用多线程进行垃圾回收,负责新生代,采用复制算法。优点是在多CPU下,STW较短。缺点是吞吐量和STW均不如G1。

CMS(Concurrent Mark Sweep)特别关注暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少用户等待时间。CMS负责老年代,采用标记清除算法。优点是垃圾回收的停顿时间短,用户体验好。
缺点:
- 会产生内存碎片化
- 在并发清理阶段无法处理”浮动垃圾“(在标记结束后和清除阶段产生的垃圾),不能做到完全的垃圾回收。
- 如果老年代内存不足无法分配对象,则CMS就会退化成单线程的Serial Old(标记整理)回收老年代。
浮动垃圾 = GC标记完成后才产生的垃圾,只能等下一轮回收。

3.PS和PO
Parallel Scavenge(PS)是JDK8默认的年轻代垃圾回收器,使用多线程并行回收,其关注的是吞吐量。具备动态调整堆内存大小的特点。采用复制算法。
优点:吞吐量高,可以手动设置。(提高吞吐量实际上是通过调整堆的参数实现的)
缺点:不能保证每次暂停时间(但可以设置最大暂停时间)
Paraller Old(PO)是为PS设计的老年代版本,也是利用多线程并行收集。采用标记整理算法。
优点:并行收集,多核CPU下效率高。
缺点:暂停时间比较长。
- PS的主要目标优先级:吞吐量 > 停顿时间
PS会优先保证吞吐量,只在吞吐量不显著受损的情况下尽量满足最大暂停时间目标。 - 年轻代大小的自动调整:PS确实会动态调整年轻代大小,但这个调整受到两个相互冲突的目标约束:
- 提高吞吐量 → 倾向于增大年轻代(减少GC频率)
- 满足暂停时间 → 倾向于减小年轻代(每次GC处理的数据量少)
- PS会在这两个目标间寻找平衡点,不一定是简单的线性关系
4.G1
JDK9之后默认的垃圾回收器是G1。
前面我们知道CMS关注暂停时间,而不能保证吞吐量。PS关注吞吐量和可控最大暂停时间,但会影响年轻代可用空间的大小(吞吐量越大,年轻代越大。最大暂停时间越小,年轻代越小)。
而G1就是将上面两种垃圾回收器的优点融合起来:
- 支持巨大的堆内存空间,并具有较高的吞吐量。
- 支持多CPU并行垃圾回收。
- 允许用户设置最大暂停时间。
G1之前,堆中年轻代和老年代的内存空间是连续的

而G1把堆划分为了多个大小相同的内存区域(Region,大小=堆内存大小/2048),区域之间是不连续的。如下图所示:

年轻代垃圾回收(Young GC)执行流程:
新创建的对象会存放在Eden区。当G1判断年轻代不足(max默认占总堆内存的60%)时,就会执行Young GC。
- 标记出Eden和Survivor区中的存活对象。
- 根据配置的最大暂停时间,G1选择性的将部分Eden和Survivor中的存活对象存放到新的Survivor区中(年龄加1),并清空这些原区域。此外,在垃圾回收时,G1会记录每个Eden区和Survivor区的平均耗时,以做为下次回收时的参考依据。
- 后续Young GC时,与之前相同,只不过Survivor区中存活的对象会被搬运到另一个Survivor区。
- 当某个对象的年龄到达阈值时,将被放入到老年代。
注意:部分对象如果大小超过Region的一半,会直接放入特殊的老年代(连续多个region都是老年代的区域),这些老年代称为Humongous区,比如堆内存4G,每个Region是2M(4G / 2048),只要某个大对象超过了1M,就放入Humongous区,如果对象过大,会跨多个Region。

混合回收
当多次回收后,会出现很多老年代区,若其总堆占有率到达阈值时,就会触发混合回收。采用复制算法,回收所有年轻代、部分老年代(选择存活度最低的区域)的对象和大对象区。
分四阶段执行:初始标记、并发标记、最终标记、并发清理
执行流程如下图所示:

注意:如果堆内存占用满了,则复制算法无法进行,只能进行Full GC(使用标记整理算法)。
3.心得
终于把JVM基础篇学完了。整理下学习流程:
首先学了类的生命周期的5个阶段(加载、连接、初始化、使用、卸载)的前三个阶段。
再通过类的加载阶段的类加载器学习,引出了双亲委派机制的学习。
之后学习了运行时数据区的划分(5大区域)以及每个区域的特点。
再然后就是对象的垃圾回收了,包括垃圾回收判断条件、垃圾回收算法、垃圾回收器。
2025.07.08
算法题:
1 | 108. 将有序数组转换为二叉搜索树 |
八股文:
java基础下
jvm学习:
开始学习jvm原理篇。
1.栈上的数据存储
字节码文件为了不同位数操作系统(32位、64位)的通用性,会有一定的空间浪费。
栈上数据存放在局部变量表中,局部变量表中每个槽的存储空间等于计算机的位数,除了long、double占用2个槽外,其余类型变量均占用一个槽。
对于32位系统来说,boolean、byte、short、char类型的数据存储均有空间浪费。
对于64位系统来说,所有数据类型均有空间浪费。(long一个槽就够,却还是用了2个槽,有一个槽的浪费)。

2.堆上的数据存储
对象在堆上的内存布局:指的是对象在堆中存放时的各个组成部分(测试代码jvm/day08/JolDemo),主要分为以下部分
- 对象头:包含标记字段(32位占用4字节,64位占用8字节)和元数据指针(指向方法区中的InstanceKlass对象)
- 对象数据:成员变量


知识点:
1.指针压缩,将元数据指针占用的字节数压缩成了4个字节,(引用数据类型变量所存储的对象内存地址也进行了指针压缩,使用4个字节进行存储)

2.内存对齐:主要是解决并发情况下CPU缓存失效的问题。缓存是按行存储数据的,当一个缓存行有多个对象进行共享,则当缓存行中的其中一个对象进行写回内存操作时,该缓存行中的数据就会失效,影响其他对象的数据获取。对象在内存中对齐后,对象缓存行之间不会互相影响。

内存对齐-字段重排列:Hostspot中,要求每个属性的偏移量Offset必须是字段长度的N倍。

3.方法调用原理
方法调用的本质就是通过字节码指令的执行,在栈上创建栈帧,并执行调用方法中的字节码指令。

静态绑定:指的是在方法第一次调用时,将方法的符号引用替换成方法内存地址的直接引用。仅适用于处理静态方法、私有方法或者适用final修饰的方法,因为这些方法不能被继承之后重写。

动态绑定:调用方法时,根据对象的对象头中的元数据指针,找到方法区中的InstanceKlass对象,获取虚方法表,再根据虚方法表找到对应的方法地址,最后调用方法。应用场景:当父类类型引用指向子类实例时,调用的方法是子类重写后的方法,而不是调用父类的方法。
如下图所示,Animal类型引用指向Cat这个子类实例对象。

注意:每个InstanceKlass对象的虚方法表存放的都是当前对象的所有方法地址,如果当前对象有父类,且没有重写父类的方法,那么虚方法表存放的是父类方法的地址,如果重写了方法,则存放重写的方法的地址。

2025.07.09
算法题:
未刷
八股文:
io流
jvm学习:
1.异常捕获原理
2.jit即时编译器原理:
将字节码指令中适用频率高的指令,即热点代码,通过jit即时编译器将其优化后,再编译成机器码存放在内存中。以便后续执行该指令时无需重新解释成机器码。
3.jit优化手段:
方法内联:直接将调用方法的字节码指令放入到当前方法的字节码指令中,避免频繁创建栈帧带来的开销。
逃逸分析:对不会从方法内部逃逸到外部的对象,进行优化,如某个对象无法逃逸到外部时,就是直接在栈上为该对象分配内存,而不需要在堆上创建该对象。
对于不逃逸的对象,JVM会进行三种优化:
- 栈上分配:直接在栈帧中分配对象内存,方法结束时自动销毁
- 标量替换:将对象分解为基本类型局部变量,消除对象本身
- 同步消除:移除对不逃逸锁对象的同步操作
① 栈上分配
1 | // 示例1:逃逸对象 |
② 标量替换(Scalar Replacement)- 更重要的优化!
1 | public class Order { |
③ 同步消除(Lock Elision)
1 | public void safeMethod() { |
4.垃圾回收器原理:
年轻代回收:卡表、写屏障、记忆集
卡表标记每个对象(如果该对象跨代引用,则标记为脏卡),写屏障用于维护卡表,记忆集记录对象的卡表位置,最后将根据记忆集进行对象扫描并标记存活。

老年代回收(混合回收):

juc学习:
1.用户线程和守护线程
用户线程指的是系统的工作线程,一般是完成业务操作的。
守护线程是一种特殊线程,主要服务其他线程的。守护线程会在后台默默执行一些系统性任务,如垃圾回收线程。
当守护线程没有服务的对象时,就无需继续运行了。假如系统只剩下守护线程时,jvm自己就会结束。
测试代码:a03_JUC/day09/DaemonDemo
2.FutureTask
futureTask实现了Runable接口,同时有可以通过传入Callable实现类来获取线程执行的返回值。
但FutureTask对于获取结果不是很友好,要么使用get()方法阻塞直至得到结果,要么通过isDone()方法手动实现轮询的方式。
测试代码:juc/day09/FutureTaskDemo
提问:FutureTask这种可以获取线程执行的返回结果,它是怎么获取的啊,异步线程怎么知道返回给它的
回答:FutureTask 把「任务执行」和「结果传递」两件事封装在同一个对象里,子线程执行完后把结果写进这个对象的内部字段,主线程随后通过同一个对象阻塞/轮询读取即可。 FutureTask 本身就是一个“结果盒子”:子线程往里放结果,主线程从里面拿结果,拿的时候如果盒子空就睡觉,等子线程写完了再被叫醒。
3.CompletableFuture
四大静态方法即其使用
1 | supplyAsync(Supplier) |
说明及测试代码:juc/day09/CompletableFutrueDemo
2025.07.10
算法题:
八股文:
juc学习:
1.函数式接口

2.Completable案例实战
多个电商平台商品价格查询模拟
代码:juc/day10/MallDemo
3.Completable相关api的使用
4.Java中的锁
迈入重头戏了。这块要非常认真学习。
5.Synchronized关键字
基础知识:
synchronized锁的类型就分为两种,对象锁和类锁!!!
对象锁锁的是类的实例对象,类锁锁的是Class对象。
synchronized关键字使用在3个地方:静态方法、成员方法、代码块。
对于代码块来说,锁的是括号中的对象。
如果修饰的是非static方法,则属于对象锁。假设一个线程访问A对象的synchronized方法,则其他线程访问A对象的其他synchronized方法时就会阻塞。并不存在说因为访问的是不同方法,所以不会阻塞,这是一个误区,没有锁方法这么小的粒度。
如果修饰的是static方法,则属于类锁。假设一个线程访问A类的实例对象B的被static修饰的synchronized方法,则其他线程访问A类的其他实例对象的被static修饰的synchronized方法时就会阻塞。
小结:对于对象锁,同一时刻,有且仅有一个线程可以访问对象的其中一个对象锁范围内的synchronized方法。类锁同理。
字节码分析:
测试代码:juc/day10/SynchronizedSourceDemo


6.公平锁和非公平锁
公平锁:线程按照申请锁的顺序来获取锁,先来先得。
非公平锁:指多个线程获得锁的顺序不是按照申请顺序来获取,可能后申请的线程比先申请的线程优先获取锁。
ReentrantLock既可以实现公平锁又可以实现非公平锁(根据构造函数的布尔值传入,默认非公平锁)。
测试代码:juc/day10/SaleTicketDemo
为什么要有公平锁和非公平锁?
非公平锁:主要是减少线程上下文切换带来的开销,因为每次切换线程上下文会导致CPU在这段时间处于空闲状态。所以非公平锁能更充分利用CPU的时间片,效率更高。如果是非公平锁,当已经持有锁的线程释放锁时,由于它正在使用CPU的时间片,所以更容易抢到下次的锁。
如果为了更高的吞吐量(在单位时间内完成任务的数量,每个任务每次只能由一个线程执行,如果是连续的任务,那么只有减少每个任务前后切换的开销时间,才能使得吞吐量上去),非公平锁比较合适。
公平锁:主要是避免线程饥饿。对于公平性要求高的系统来说更合是。
为什么默认非公平锁?
因为大多数情况下,更注重性能,效率优先。
7.可重入锁
可重入锁指的是一个线程在外层方法获取锁时,会再进入该线程内层方法时会自动获取锁(前提:锁对象时是同一个),不会阻塞。
相关测试代码:juc/day10/ReEnterLockDemo
隐式锁
synchronized是隐式锁,会自动在进入内层方法(即调用对象的其他synchronized方法)时自动给该对象锁监视器的重入次数字段加1,退出内层方法时重入次数减1。
显式锁
ReentrantLock是显式锁,需要手动进行lock()和unlock()。
2025.07.11
算法题:
1 | //236. 二叉树的最近公共祖先 |
八股文:
无
juc学习:
1.死锁案例
juc/day11/DeadLockDemo
2.线程中断及协商机制
通过volatile实现
通过AtomicBoolean实现
通过Thread类自带的api实现
mongodb学习:
导师说要学习mongodb,所以速通了,基本用法都会了,java使用mongodb的基本api也都掌握了。
2025.07.12
算法题:
5道
八股文:
juc学习:
1.线程中断相关方法
线程的中断不是用于停止线程的,而仅仅是将线程中断的标志位置为true,至于被中断后,被中断的线程怎么处理全看线程自身(比如while循环判断中断标志位是否为true,如果true就return,其他线程干预不了。

2.线程的阻塞和唤醒机制
相关代码:juc/day12/LookSupportDemo
1.synchronized关键字
结合锁对象的wait()和notify()方法
2.ReentrantLock
结合其Conditon对象( 维护了一个属于当前锁的“等待队列”)的await()方法和signal()方法
ReentrantLock 和 Condition 的关系
- 每个 Condition 实例是通过 ReentrantLock.newCondition() 创建的;
- 它绑定于一个具体的 Lock(即 ReentrantLock);
- 多个 Condition 可以绑定同一个锁,但它们各自维护自己的等待队列。
Condition对象的await()方法:
- 释放当前线程持有的Condition对象关联的ReentrantLock的锁;
- 将当前线程加入这个 Condition 内部的等待队列中;
- 线程进入等待状态,直到被其他线程唤醒(通过 signal() 或 signalAll());
- 被唤醒后重新尝试获取锁,之后继续执行。
Condition对象的signal()方法:
- 从该 Condition 的等待队列中取出一个线程(不一定是最早等待的那个);
- 将它移动到与 ReentrantLock 关联的 同步队列(AQS 队列) 中并唤醒;
- 被唤醒的线程会在后续尝试重新获取锁并继续执行。
3.LockSupport
LockSupport的静态方法**park()和unpark()**进行获取许可证和发放许可证。
LockSupport利用了许可证的概念,来做到阻塞和唤醒线程的功能,每个线程都有一个许可,但与Semaphore不同的是,许可的累加上限是1,即在未消费许可的情况下,无法叠加许可,连续重复的unpark最终也只有一个许可。
LockSupport的特点是代码编写更简洁,只需调用LockSupport的静态方法即可,且可以通过传参指定唤醒的线程。
注意:两个线程都创建的情况下,t2可以提前对t1进行unpark,则t1调用park后不会阻塞。
后续路线规划:
基础内容:懂为什么 > 懂是什么。比如,我问你分代GC优势是什么,你现在答得上来吗。
框架题:懂流程(核心流程、设计思路) > 记源码(不用逐行记忆)
场景题:学思路 > 背答案。先多了解了解各种场景的解决方案,然后独立思考解决方案。
2025.07.13
算法题:
八股文:
1.进程和线程的区别是什么:
进程是系统运行程序的基本单位。一个程序的运行对应了一个进程从创建、使用到消亡的过程。此外进程与进程之间内存互不共享。
而线程是进程一个执行单元,是CPU分配和调度的基本单位。一个进程可以有多个线程。在同一个进程下的多个线程,共享进程的方法区和堆区,但线程也有其私有的pc、本地方法栈和虚拟机栈。
此外,同一个进程下,线程上下文切换的开销要远小于进程的上下文切换,所以线程也被称为轻量级进程。
比方说,在java里启动main函数时,其实就是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程。
2.进程和线程的关系及优缺点:
线程和进程的最大不同在于,基本上各个进程之间是相互独立,而同一个进程下的多个线程之间是极有可能相互影响的。此外线程执行开销小,但不利于资源的管理和保护,进程则反之。
3.为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器:线程切换之后,能够恢复到原来的执行位置。
虚拟机栈和本地方法栈:保证线程中的局部变量不被别其他的线程访问到
juc学习:
1.Java内存模型(JMM)
JMM本身是一种抽象的概念,并不真实存在,仅仅描述的是一组约定或规范,通过这组规范,定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入,如何以及何时变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
2.JMM三大特性
原子性:一段代码的执行,要么全部执行成功,要么全部执行失败。
可见性:是指当一个线程修改了某一个共享变量的值,其他线程能否立即知道该变更。(JMM规定了所有变量都存储在主内存,也就是堆区中,但每个线程都有自己的工作内存,也就是栈空间,读写变量时会先将变量拷贝到工作内存中)
有序性:程序执行顺序按照代码顺序进行(即使指令重排序也不会影响最终结果,但在多线程环境下,这种重排序可能导致问题,故需要用到volatile关键字进行禁止指令重排序)
3.happens-before原则
happens-before是一种偏序关系,用于描述两个操作之间的可见性顺序,比如,操作Ahappens-before操作B,那么A的执行结果对B是可见的(无论是否在同一个线程中)。
有八大规则,了解即可,差不多都是通俗的常识
2025.07.14
算法题:
八股文:
juc学习:
1.volatile关键字
被volatile关键字修饰的变量具有两大特点:可见性和有序性(通过插入内存屏障,禁止指令重排序)
(1)可见性
测试案例:juc/day14/VolatileSeeDemo
- 写操作:当一个线程修改volatile变量时,JVM会将该变量的新值从本地内存立即刷新到主内存。
- 读操作:当一个线程读取volatile变量时,JVM会直接从主内存读取该变量的值,而不是使用本地内存中的副本。
注意:
1.线程操作共享变量时,通常会先将变量从主内存复制到自己的工作内存中,然后操作后再写回主内存。线程的工作内存与jvm的堆、栈并不是一个层次上的内存划分,如果非要类比的话工作内存对应虚拟机栈的部分区域。
2.不使用 volatile 时,变量从本地内存刷新到主内存需要依靠显式的同步机制(如 synchronized、并发工具类、线程控制方法等)来触发。否则,变量可能一直缓存在本地内存中,不会立即同步到主内存,造成可见性问题。
3.需要本地内存,是为了提高程序的执行效率,通过缓存机制加速数据访问。主要原因1.主内存访问速度慢。2.提高指令的并行性。
(2)有序性
通过插入内存屏障进行保证。
- 写操作前插入 StoreStore 屏障:保证前面的普通写与之后的 volatile 写不会重排;
- 写操作后插入 StoreLoad 屏障:保证 volatile 写与后续可能的读操作不会重排;
- 读操作前插入 LoadLoad 屏障:保证前面的普通读与之后的 volatile 读不会重排;
- 读操作后插入 LoadStore 屏障:保证 volatile 读与后续的写操作不会重排。
(3)不保证原子性
测试案例:juc/day14/VolatileAutomicDemo
(4)使用场景
1.懒汉双重锁单例模式。
2.多线程情况下的状态标志位
3.写加锁,而读不加锁
- 进入同步块前,从主内存读取共享变量;
- 退出同步块时,将本地内存中的变量刷新回主内存;
2.内存屏障
什么是内存屏障?
内存屏障(Memory Barrier,也叫内存栅栏)是 CPU 指令中的一种机制,用于控制指令执行顺序,防止编译器或 CPU 对内存访问操作进行重排序。
在 Java 中,它被 JVM 用来实现 JMM(Java Memory Model)的可见性和有序性保证。
内存屏障有什么用?
现代计算机为了提高性能,会做以下优化:
- 编译器优化:将代码中的读写顺序重新排列;
- CPU 乱序执行(Out-of-Order Execution):实际执行顺序与程序逻辑不一致;
- 缓存一致性问题:不同线程看到的变量值可能不一致;这些优化可能导致多线程并发时出现不可预期的结果,比如:
- 线程 A 修改了变量 X 和 Y;
- 线程 B 可能先看到 Y 的变化,后看到 X 的变化;
- 这违反了程序的逻辑顺序。
因此,我们需要使用 内存屏障 来干预这种行为,确保某些操作按照我们期望的顺序发生。
内存屏障的四大类型
| 类型 | 功能描述 |
|---|---|
| LoadLoad | 确保屏障前面的读操作在后面的读操作之前完成 |
| StoreStore | 确保屏障前面的写操作在后面的写操作之前完成 |
| LoadStore | 确保屏障前面的读操作在后面的写操作之前完成 |
| StoreLoad | 最强大的,确保屏障前面的写操作在后面的读操作之前完成 |
- volatile写操作:
- 在每个volatile写操作前插入StoreStore屏障,确保前面的普通写操作先于volatile写操作执行。
- 在每个volatile写操作后插入StoreLoad屏障,防止volatile写与之后可能有的volatile读/写重排序。
- volatile读操作:
- 在每个volatile读操作前插入LoadLoad屏障,禁止之后所有的普通读操作和volatile读操作重排序。
- 在每个volatile读操作后插入LoadStore屏障,禁止之后所有的普通写操作和volatile读重排序。
3.CAS
我感觉对着juc视频记录内容很乱,不像jvm,课程质量比较低。所以后续就简略记录了。
CAS(Compare and swap)底层都是调用UnSafe类的方法,而UnSafe类的方法都是native修饰的。
AtomicInteger类、AtomicReference引用类、AtomicStampedReference(利用版本号)的使用。
测试代码:juc/day14
CAS的原子性本身就是使用硬件层面的指令来保证的。
CAS缺点:循环时间可能很长、ABA问题
4.手写自旋锁
juc/day14/AtomicReferenceDemo
5.ThreadLocal
多人销售房子案例:juc/day14/ThreadLocalDemo
ThreadLocal的set()方法实际就是给当前线程的ThreadLocalMap变量放入键值对,以当前ThreadLocal对象为key,传入的参数为value。
线程复用场景下,每次线程用完后就需要调用ThreadLocal的remove方法,清除当前任务下的值,以免后续线程复用时,之前的数据还保留着。
阅读分析了ThreadLocal的方法源码。
6.Thread、ThreadLocal、ThreadLocalMap三者的关系
2025.07.15
算法题:
八股文:
juc学习:
1.ThreadLocalMap中的Entry
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
Entry继承弱引用的原因
防止ThreadLocal实例一直驻留在内存,造成内存泄漏:避免ThreadLocal对象在外部销毁或则无法引用时,因为ThreadLocalMap中的Entry中的key引用了这个ThreadLocal而导致这个ThreadLocal对象无法被垃圾回收。(弱引用指的是当一个对象仅被弱引用对象关联时,无论是否内存充足,只要进行了垃圾回收,则其关联的对象就一定会被回收)
清除脏Entry
当回收掉ThreadLocal对象后,key为null,但其关联的value值仍然被Entry强引用,为了避免遗留下来的value被其他ThreadLocal访问到,需要对这个脏Entry进行清空处理。
处理方式:调用ThreadLocal对象的set()、get()、remove()方法时会自动清除部分key为null的entry部分。
1 | // expunge entry at staleSlot |
2.ThreadLocal使用建议
1.创建ThreadLocal时使用withInitial()方法进行创建,这样避免在没有set情况下使用get时获取到null(get时,如果ThreadLocalMap中的Entry没有持有该ThreadLocal对象的引用时,就会将withInitial中的supplier返回值进行set,然后再取出数据)。在某些业务下可能会产生空指针异常。
2.建议把ThreadLocal修饰为static。
3.用完切记手动remove()。
3.对象的内存布局
对象头:包含标记字段Mark Word(32位占用4字节,64位占用8字节)和元数据指针(指向方法区中的InstanceKlass对象)
对象实例数据
对齐填充:用于保证对象的起始地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍。
4.Mark Word
以64位为准,32位没多大意义了。
2025.07.16
算法题:
八股文:
并发编程上、并发编程中
juc学习:
1.synchronized的锁升级
无锁 -》 偏向锁 -》轻量锁 -》重量锁
无锁-》轻量级锁 -》重量级锁的测试代码:
juc/day16/SynchronizedDemo
2.偏向锁
原理
偏向锁类似于非公平锁,锁总是被第一个占用它的线程拥有。当锁在第一次被获取时,会在moniter中记录当前偏向线程的id,如果后续这个线程进入和退出锁的代码块或者方法时,不需要再次进行加锁和释放锁,而是会直接去检查锁的mark word里面存放的是不是自己的线程id,如果是,则不做任何处理,如果不是,则表明发生了竞争。即没有其他线程来竞争时,那么这个锁就不需要重新获取,线程id一直记录在对象的moniter的mark word中,直到竞争发生才尝试利用CAS去替换mark word中的线程id。因此偏向线程长时间持有着锁,避免了同个线程频繁利用CAS去更新对象头来加解锁,从而提升性能。
竞争流程
当第二个线程来抢锁 → 触发安全点 → 检查原线程状态 → 撤销偏向锁 → 升级为轻量级锁 → 后续可能再升级为重量级锁。
竞争成功时(偏向线程不在执行锁的代码块),markword里的线程id为新线程的id,锁不会升级,仍然为偏向锁。
竞争失败时(偏向线程正在执行锁的代码块),为了保证线程间的公平性,会撤销偏向锁,并升级为轻量级锁,但仍然被原线程持久并等待释放。
优点
加锁和解锁不需要额外的消耗
缺点
如果线程间存在锁竞争,会带来额外的锁撤销消耗
适用场景
适用于只有一个线程访问同步块的场景
注意
java15逐步废弃了偏向锁,并在JDK18彻底删除。
主要原因还是维护成本太高。
个人总结
3.轻量级锁
原理
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。
然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
适应性自旋
自旋是需要消耗 CPU 的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费 CPU 资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环 10 次,如果还没获取到锁就进入阻塞状态,同时这个锁就会升级成重量级锁。
但 JDK 采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
注意CAS的规则。
HotSpot 的实现里,轻量级锁的 CAS 有一个 隐含条件:
- 只有当 Mark Word 处于“无锁状态”(未偏向、未锁定)时,CAS 才会尝试替换。
- 如果 Mark Word 已经是 其他线程的 Lock Record 指针,JVM 会 直接判定竞争失败。
优点
竞争的线程不会阻塞,提高了程序的响应速度
缺点
如果线程始终得不到锁,频繁自旋会耗费CPU
适用场景
追求响应时间,且同步块执行速度块的场景
个人总结
轻量级锁通过CAS机制,会试着将锁对象的对象头中的mark word内容复制并存放到当前线程的栈帧空间(Lock Record)中,然后将mark word的内容改为指向该空间的指针。这里的CAS机制是会先查看锁对象的状态,如果对象处于无锁状态或者偏向锁状态,则尝试使用CAS修改mark word,否则直接进入下一次自旋。当自旋次数到达一定次数后,轻量级锁就会膨胀为重量级锁。
轻量级锁主要还是适用于线程竞争小的场景。尽管线程不会阻塞,但如果线程竞争大,且同步代码块的执行时间又过长,就会导致频繁自旋,浪费CPU资源,效率实际上还不如使用重量级锁。
4.重量级锁
原理
重量级锁依赖操作系统提供的互斥锁(mutex)实现。当线程获取重量级锁时,操作系统需要从用户态切换到内核态,检查mutex是否被占用,如果被占用,则会将当前线程挂起并进入阻塞状态,进而触发线程的上下文切换。由于涉及用户态与内核态的切换以及线程调度的开销,因此性能消耗较大。
当锁膨胀为重量级锁时,不再依赖线程栈中的 Lock Record,而是由 JVM 内部的 ObjectMonitor 对象来管理锁状态和等待队列。
🧱 ObjectMonitor 是什么?
它是 HotSpot JVM 中用于实现重量级锁的核心结构;
每个被膨胀为重量级锁的对象,都会关联一个 ObjectMonitor 实例;
ObjectMonitor 包含以下关键字段:
- _owner:当前持有锁的线程;
- _WaitSet:调用 wait() 的线程等待队列;
- _EntryList:等待获取锁的线程队列;
- _recursions:重入次数;
- _markWord:保存加锁前的 Mark Word 值;✅
3️⃣ 重量级锁是如何处理 Mark Word 的?
当锁升级为重量级锁后,如果线程要获取锁,流程如下:
- 对象头 Mark Word 被修改为指向 ObjectMonitor 的指针;
- ObjectMonitor 保存原始的 Mark Word(包括哈希码、GC 信息等);
- 当锁释放时,JVM 会从 ObjectMonitor 中恢复原来的 Mark Word;
- 如果对象后续需要无锁操作或 GC,这些原始信息就非常重要。
优点
线程竞争不会自旋,从而不会消耗CPU
缺点
线程阻塞,响应时间缓慢
适用场景
追求吞吐量,同步块执行时间长的场景
个人总结
5.锁的升级流程
首先,一个对象刚被创建时,处于无锁状态,对象头的mark word记录的是对象的哈希码、GC年龄等。
当第一个线程尝试获取锁时,jvm会将锁升级为偏向锁,此时markword中记录了获取锁的线程的id,只要没有其他线程来竞争,该线程每次执行代码块都不需要加锁和释放锁,性能最优。
当第二个线程尝试获取锁时,且第一个线程在使用同步代码块时,偏向锁就会被撤销,并升级为轻量级锁。此时,占有锁的线程就会在自己的栈帧中创建一个LockRecord来存放锁对象的mark word信息并将mark word的内容替换为指向LockRecord的指针。第二个线程会不断尝试通过CAS操作将mark word中的内容替换为自己的Lock Record的指针。如果CAS失败则会进入自旋重试。
当自旋次数到达JVM设定的阈值时,就会把轻量级锁膨胀为重量级锁。利用操作系统的mutex来实现线程的阻塞和唤醒。
回答注意点:升级过程+mark word变化。
6.AQS开篇
2025.07.18
算法题:
八股文:
juc学习:
1.AQS源码分析
分析了源码,大致了解了aqs的原理,主要还是跟链表相关,其中线程的阻塞和唤醒用到了LockSupport的许可机制,后续还是直接背八股吧。
2.读写锁–ReentrantReadWriteLock
测试代码:juc/day18/ReentrantReadWriteLock
相比于ReentrantLock(读读、读写、写写均互斥),ReentrantReadWriteLock实现了多个读共存,避免了读与读之间的相互阻塞等待,适用于读多写少的场景。
缺点:
1.在读多写少场景下,易导致写线程饥饿。
2.要注意锁降级问题
3.锁降级
跟synchronized的锁升级不同。
锁降级 是指:当前线程在持有写锁的情况下,可以获取读锁,之后再释放写锁,这样可以保证在写锁释放之后,不会出现其他线程修改数据。
4.邮戳锁
测试代码:juc/day18/StampedLockDemo
无锁 -》 独占锁(ReentrantLock) -》读写锁(ReentrantReadWriteLock) -》邮戳锁(StampedLock)
StampedLock不仅具有实现读写锁的功能,还可以通过乐观读来解决写锁饥饿的问题。
例如:可以在读的过程中,利用乐观读获取stamp,然后读取相应数据,之后再调用validate(stamp)查看当前读取到的数据版本是否一致,一致则正常结束,否则可以循环读取,直至过程中数据没有被修改。
🧠 乐观读的优势:
- 不阻塞写锁:乐观读不加锁,允许写线程随时获取写锁,避免了写线程长时间等待;
- 缓解写锁饥饿:即使有大量读线程在执行,写线程也能及时获取锁进行修改;
- 轻量高效:适用于读多写少、对一致性要求不高但对性能敏感的场景
缺点:
1.不支持重入: 一个线程不能重复获取读锁或写锁,否则会死锁或抛异常
2.不支持条件变量(Condition):不能像 ReentrantLock 一样配合 Condition 使用
3.不支持中断:使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法。
2025/08/26
补充版本戳变更机制
- StampedLock 的版本戳(stamp)在每次获取写锁时都会发生变化
- 无论写操作是否真正修改了数据,只要获取了写锁,就会导致版本戳失效
- 这是 StampedLock 的设计原则,确保数据一致性
5.完结撒花
juc学完了,知识点确实多,但其实都是串联起来的,还是要多消化消化,下一站,深入mysql !!!。
2025.07.20
昨天玩了一天了。。。。不能摆了
算法题:
八股文:
mysql学习:
1.全局锁
全局锁会锁整个数据库实例。
相关命令
1 | flush table with read lock;//开启全局锁 |
数据库备份会涉及到全局锁,相关命令如下
1 | //利用mysqldump进行数据备份 |
2.表级锁
表级锁,每次操作会锁住整张表。
表级锁主要分为3类
(1)表锁
表锁分为共享锁(所有客户端都可读,但都不可写)和排他锁(当前客户端可读可写,但其他客户端不可读和写)
语法
1 | //加锁 |
(2)元数据锁
在增删改查和修改表结构时,会自动加上元数据锁。
主要是避免DML和DDL的冲突。

1 | //查询元数据锁信息 |
(3)意向锁
在添加行锁时会自动添加意向锁。用于添加行锁后,告诉表锁,根据互斥性判断表锁是否可以添加成功。避免在尝试获取表级锁时需要逐行检查行锁。
- 意向共享锁(IS):与表级共享锁(read)兼容,与表级排他锁(write)互斥。
- 意向排他锁(IX):与表级共享锁和排他锁都互斥。
注意:意向锁之间不会互斥。
1 | //查询意向锁和行锁 |
3.行级锁
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
(1)行锁
锁定单个行记录,防止其他事务对此行进行update和delete。
默认情况下,innoDB在RR级别下运行,使用临键锁进行搜索和索引扫描,以防止幻读。
- 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,会自动优化为行锁。
- InnoDB的行锁是针对索引加的锁,不通过索引条件检索数据,那么InnoDB就会对表中的所有记录加锁,此时相当于升级为表锁。

(2)间隙锁
锁定索引记录间隙,确保索引记录间隙不变,避免了其他事务在这个间隙中insert而产生幻读。
间隙锁记录的是右边界的主键数据。即该主键和上一个主键之间锁住。


(3)临键锁
行锁和间隙锁的组合。既锁住数据,又锁住数据前的间隙。
2025.07.21
今天把科目一过了,94一遍过(●ˇ∀ˇ●)
算法题:
八股文:
mysql学习:
1.存储引擎
存储引擎就是对于存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的,故也可以称为表类型。

表在创建时如果没有指定,会隐式使用InnoDB存储引擎
1 | -- 查看表的创建语句 |
2.InnoDB存储引擎
InnoDB是兼顾高可靠性和高性能的通用存储引擎,在MYSQL5.5之后,是默认的存储引擎。
特点
DML操作遵循ACID模型,支持事务。
支持行级锁,提高并发性能。
支持外键FOREIGN KEY约束,保证数据的完整性。
表空间文件
xxx.ibd:xxx代表表名,innoDB每张表都会对应这样一个表空间文件,存储该表的结构、数据和索引。
一般存放位置(所有存储引擎都一样)
C:\ProgramData\MySQL\MySQL Server 8.0\Data
1 | -- 查看是否开启一表一空间文件,ON表示开启,FALSE表示所有表共用一个空间文件 |
表空间结构如下:

3.MYISAM存储引擎
MYISAM是MySQL早期的默认存储引擎
特点
- 不支持事务,不支持外键
- 支持表锁,但不支持行锁
- 访问速度快
文件
xxx.sdi:存储表结构信息
xxx.MYD:存储数据
xxx.MYI:存储索引

4.Memory存储引擎
Memory引擎的表数据存储在内存中,由于会受到硬件问题、或断电问题的影响,只能做为临时表或缓存使用。
特点
内存存放
支持hash索引(默认)
文件
xxx.sdi:存储表结构信息
因为数据和索引在内存,所以没有其他文件。
三大存储引擎区别

show engines;

5.存储引擎的选择
InnoDB存储引擎支持事务、行级锁和外键。如果对数据完整性有较高要求、在并发场景下要求保证对数据的一致性,以及操作数据时涉及到大量的更新、删除操作,那么InnoDB是最合适的。
MyISAM在以读和插入操作为主,很少更新或者删除操作的场景、对事务的完整性、并发要求不高,则可以选择MyISAM。但现在一般被MongoDB取代了。

6.索引
索引是帮助MySQL高效获取数据的数据结构(有序)。

7.索引结构
B树
B树又称B-树。
所有节点都存储数据。
B+树
B+树相对于B树的区别
- 只有叶子节点存储数据
- 叶子节点之间形成了一个单向链表(Mysql中做了优化,是双向循环链表)

| 对比项 | B树 | B+树 |
|---|---|---|
| 数据存储 | 所有节点 | 仅叶子节点 |
| 叶子节点 | 无链表结构 | 双向链表连接(支持顺序遍历) |
| 范围查询 | 效率低 | 效率高(链表遍历) |
| 磁盘I/O | 不稳定(可能跨层访问) | 更稳定(数据集中在叶子层) |
| 树高 | 可能较高(内部节点存储了数据,每个节点可容纳的子节点的空间更小) | 更矮(扇出更大) |
| 数据库应用 | 较少 | 主流(InnoDB、MongoDB) |
Hash索引
- 只能进行等值匹配,不支持范围查询。
- 无法利用索引完成排序操作。
- 查询效率高,通常只需要一次检索就可以了。

为什么InnoDB存储引擎选用B+树索引结构?
相对于二叉树,层级更少,搜索效率高(但对于大量更新索引字段的情况下,需要保持多路平衡的开销更大);
相对于B树,(1)由于节点以页为单位进行存储,B树无论叶子节点还是内部节点,都会保存数据,导致内部节点可存储的子节点指针空间减少,即子节点指针个数减少。所以树更高,查询性更低。(2)B+树叶子节点之间构成了双向循环链表,支持高效的范围查询,而B树需要回溯到父节点进行查询,效率更低。
相对于Hash索引,B+树支持范围查询和排序操作。
8.索引分类
根据索引的存储形式,分为聚簇索引和二级索引。
| 分类 | 含义 | 特点 |
|---|---|---|
| 聚簇索引 | 将数据和索引存放到一起,索引结构的叶子节点保存了行数据 | 必须有,而且只有一个 |
| 二级索引 | 索引结构的叶子节点保持的是对应的主键 | 可以存在多个 |
聚簇索引选取规则:
- 如果存在主键,则主键索引就是聚簇索引
- 如果不存在主键,则使用第一个唯一索引做为聚簇索引
- 如果没有主键和唯一索引,则innoDB会自动生成一个rowid作为隐藏的聚簇索引

回表查询:指的是先根据二级索引查找到对应的主键值,然后根据主键值从聚簇索引中查找对应的行数据。
9.查看sql的执行频次
1 | -- session 是查看当前会话 ; |
10.查看慢查询日志
1 | -- 查看慢查询日志是否开启 |
11.SQL性能分析
(1)show profiles
1 | -- 查看当前MYSQL是否支持profile操作 |


(2)explain
Explatin可以查看SQL 语句的执行计划
EXPLAIN select ak.* from attr_key ak where ak.id in (select attr_key_id from attr_value where name LIKE '%平')

EXPLAIN执行计划各字段含义:
- type: 连接类型,表示查询的效率,类型越好性能越高。


重点关注这五个字段type、possible_key、key、key_len、rows、extra(额外信息)
possible_key表示可能可以应用在当前查询语句中的索引,当possible_key不为null,而key为null时,告诉我们当前sql语句优化后可能可以走possible_key中的索引。
key_len表示实际查询过程中使用到的索引的总字节长度。可以衡量联合索引下使用到的索引有哪些,联合索引中使用部分索引的字节长度小于使用全部索引的字节长度。
extra 字段提供了查询执行计划中的额外信息,用于帮助理解 MySQL 如何实际执行查询。
| Extra 值 | 含义 | 常见场景 | 性能影响 |
|---|---|---|---|
Using index |
覆盖索引,无需回表 | SELECT 的列全部包含在索引中 |
✅ 高效 |
Using where |
用 WHERE 过滤,需回表 | 非覆盖索引或需额外列 | ⚠️ 中 |
Using index condition |
索引条件下推(ICP) | WHERE 部分条件可在索引层过滤 |
✅ 高效 |
Using filesort |
需要额外排序 | ORDER BY / GROUP BY 无可用索引 |
❌ 慢 |
Using temporary |
需要临时表 | GROUP BY、DISTINCT 无法走索引 |
❌ 慢 |
Using join buffer (Block Nested Loop) |
JOIN 被驱动表无索引 | 大表 JOIN 无索引字段 | ❌ 慢 |
Range checked for each record |
对每行重新评估索引范围 | 极少见,索引选择性差 | ⚠️ 中 |
Impossible WHERE |
WHERE 条件恒假 | WHERE 1=0 |
✅ 无实际开销 |
Select tables optimized away |
查询被完全优化掉 | 仅聚合函数且无行需扫描 | ✅ 高效 |
Distinct |
提前终止去重 | SELECT DISTINCT 走索引 |
✅ 高效 |
Scanned N databases |
扫描多个库 | INFORMATION_SCHEMA 查询 |
⚠️ 中 |
Full scan on NULL key |
子查询无法使用索引 | 子查询回退全表扫描 | ❌ 慢 |
当 Extra 里同时出现 Using index; Using where 时,意味着:
本次查询只用扫描索引页就能拿到所需列(不回表),但索引扫描出来的记录还需要由 server 再按剩余条件过滤一次。
12.最左前缀法则
13.索引失效情况
1.使用范围查询(>,<)。>=和<=不会失效
2.在索引列上使用运算操作
3.字符串类型索引使用时,不加引号
4.头部模糊查询(除头部以外,索引不会失效)
5.or连接条件下,左右条件只要有一个没有索引就失效
6.如果mysql评估使用索引比全表扫描慢(在索引命中的数据占大量的情况下),则不走索引。
2025.07.22
算法题:
八股文:
讲MVCC:
理清思路
1、MVCC是什么,用来解决什么问题的,是怎么实现的。
2、undolog版本链、readview是怎么回事。
3、四种隔离级别是怎么实现的。
4、什么是当前读、快照读。
我的回答
MVCC就是指多并发版本控制,MVCC的主要作用就是维护了一个数据的多个版本,来解决并发场景下的读写冲突的问题。并发访问数据的场景分为三类,首先是读读,因为只是读取数据,所以没有任何并发安全问题。然后是读写,读写并发的场景下主要会出现脏读、不可重复读和幻读问题。最后是写写并发,会出现丢失更新问题。
对于脏读、不可重复读和幻读问题,可以通过四大隔离级别来解决。这四大隔离级别中,RC和RR就利用到了MVCC来实现的。
而MVCC的主要就是通过隐藏字段、undolog和readview来实现的。
undolog记录了数据各个版本(具体实现就是记录和每次操作相反的sql语句,最新数据版本可以通过执行当前回滚指针指向的sql语句来回退到上一个版本),而每条数据都会有三个隐藏字段,分别是修改当前数据的事务id,回滚指针以及隐藏主键。当对数据进行修改操作时,就会记录当前事务的id,并把修改前的数据放到undolog日志中,并在回滚指针中记录在undolog中该数据上次版本的地址。通过这个回滚指针,就形成了该数据的版本链。而readview则记录了对当前事务来说活跃的事务id(即未提交的事务id),包含了活跃事务id集合、未来id值,活跃的最小事务id和当前事务id。readview主要是配合快照读来使用的。
在读未提交的隔离级别在,没有用到MVCC,直接去读取最新数据。这就会产生脏读问题。
在读已提交的隔离级别下,每次读取都会生成一个新的readview,根据readview来获取当前事务可见的最新的已提交的数据版本,从而解决了脏读问题。但每次读取都生成readview就会导致在自己未修改数据而其他事务修改了该数据的情况下,读取的数据前后不一致的问题。
在可重复读的隔离级别下,只有在第一次查询时才会生成readview,后续的select都会读取同一个数据版本。这样就避免了不可重复读问题。但仍然会出现幻读问题,主要原因是普通的select语句是快照读,复用了第一次读取到的readview,避免了幻读问题,而增删改操作是当前读,读取的是最新的已提交的数据版本,故会产生幻读问题。解决方法是在第一次使用select时,用当前读添加临键锁或者间隙锁(数据未命中的情况)来获取数据,也就是使用select … for update,在读取到的数据的索引之间的间隙加锁。从而解决幻读问题。
在串行化的隔离级别下,没有用到MVCC,事务与事务之间是串行执行的。
至于写写并发问题,就通过给数据加排他锁来解决。
wc,我感觉自己写很不错了,感觉连贯讲到了很多知识点。通起来了。
mysql学习:
1.索引设计原则

2.InnoDB存储引擎–事务实现原理

redolog
重做日志记录的是事务提交时数据页的物理修改,由于实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲和重做日志文件,前者在内存,后者在磁盘中。当事务提交后,会把所有修改信息存到日志文件中,用于在刷新脏页(已经被修改但尚未写入磁盘的内存页)到磁盘发生错误时,进行数据恢复使用。

WAL(Write-Ahead Logging):先写日志机制,即先将日志写入磁盘后再把脏页的数据刷新到磁盘。因为日志文件是以追加的形式加入磁盘,是顺序IO,效率更快,而脏页的数据需要随机IO进行修改,效率更慢。所以先写入日志文件,以便脏页刷新错误时,可以利用日志文件进行数据恢复。
undolog

3.MVCC
MVCC(Multi-Version Concurrency Control),多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MVCC提供了非阻塞读的功能。其具体实现,还依赖于数据库中的三个隐式字段、undo log日志、readView。
当前读
读取的是最新版本,读取时需要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如select … lock in share mode(加共享锁),select … for update、 update、insert、delete(默认加排他锁)都是一种当前读。
快照读
简单的select就是快照读,读取的是数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
Read Commited(读已提交): 每次select,都生成一个快照读。
Repeatable Read(可重复读): 开启事务后,以第一个select语句读到数据版本为准。
Serializable(串行化): 快照读会退化为当前读。
隐藏字段
MVCC包含了三个隐藏字段,当前事务id、回滚指针、隐藏主键。
| 隐藏字段 | 含义 |
|---|---|
| DB_TRX_ID | 最近修改数据的事务ID,记录插入这条记录或最后一次修改该记录的事务ID |
| DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本 |
| DBROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段 |
undolog
undolog回滚日志,在insert、update、delete的时候产生便于数据回滚的日志。
当insert的时候,产生的undo log(空数据)只在回滚时需要,在事务提交后,可被立即删除。
当update、delete的时候,产生的undolog日志不仅在回滚时需要,在快照读的时候也需要,不会被立即删除。

readview
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护了当前活跃(未提交)的事务ID。

事务在快照读时,会根据生成的ReadView判断当前可读的数据版本。
2025.07.23
算法题:
八股文:
redis学习:
1.SQL和NoSQL的区别

语言组织:
存储结构、数据关系、ACID原则、适用场景。
关系型数据库,就是建立在关系模式基础上的数据库。采用表格结构存储数据,强调数据关系的严谨性,且遵循ACID原则来保证数据的强一致性,适合处理结构化数据和对数据安全性和一致性要求高的场景,但在海量数据和高并发场景下可能会遇到扩展瓶颈。
而非关系型数据库则采用灵活的数据模型,比如键值对、文档等来存储数据,但会牺牲部分一致性来换取高性能和高扩展,适合处理对性能要求高的非结构化数据,其水平扩展性高。
两者在业务场景下有各自的侧重点:关系型数据优先保证数据的准确,而非关系型则更侧重系统性能和扩展能力。
mysql学习
突然发现我的路线出大问题,虽然学了jvm和juc,但都太浅显了,需要结合八股文多多理解。看了优雅哥的视频,真tm精辟,醍醐灌顶,就是要总结自己的一套体系笔记,多多追问,多多思考,不然真得完蛋,视频教的都是固化知识,缺少了自我深度思考,经不起拷打的!!!
1.SQL优化
insert优化
批量插入>多次插入
手动提交事务。在mysql中每次insert都会自动提交事务,如果要多次插入,就会频繁提交事务,这是不必要的性能开销。
主键顺序插入
主键优化
主键设计原则
- 满足业务需求情况下,主键长度尽可能降低
- 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键
- 尽量不要使用UUID(随机值)或者其他自然主键,如身份证号
- 避免对主键的修改
Mysql中数据行是以主键为序、页为单位进行存储的,在数据插入时会涉及到页分裂和页合并。如果是非主键顺序插入数据,则会造成页分裂现象。当删除数据时,会自动进行页分裂。
order by 优化
索引排序 = 利用索引本身的有序性把“排好序的主键”先拿到手;如果查询列不全在索引里,就再拿这些主键回表取完整行。
可以通过执行计划查看执行效果
using filesort:表示mysql需要在排序缓冲区对数据进行额外排序。
using index:表示可以通过有序索引顺序直接返回所需数据。

优化规则
- 根据排序字段,建立合适的索引,多字段排序时,遵循最左匹配原则。
- 尽量使用覆盖索引。
- 多字段排序时,一个升序一个降序,此时需要主力联合索引创建时的规则。
- 如果不可避免出现filesort,需要大量数据排序是,可以适当增加排序缓冲区的大小。
1 | -- using filesort。即使可以走索引排序,但仍然需要回表查询,成本高,所以优化器干脆直接全表扫描+filesort了 |
group by 优化
using temporary:表示mysql需要创建一张临时表来完成Group by/distinct / order by / union 等操作,无法直接利用索引一次性得到结果
可以通过索引来提高分组效率,且索引的使用也是满足最左前缀法则的。
limit优化
mysql的limit语句在偏移量很大时,查询效率很慢,因为需要先查询offset+pagesize行数据后,再丢弃offset行数据。如果是
1 | select * from test where val=4 limit 300000,5; |
就需要回表查询这300005条记录后,再排序,再选择最后5条数据,这就导致了大量的 CPU 和内存消耗,在内存中进行排序和过滤。
使用索引覆盖扫描。
如果我们只需要查询部分字段,而不是所有字段,我们可以尝试使用索引覆盖扫描,也就是让查询所需的所有字段都在索引中,这样就不需要再访问数据页,减少了随机 I/O 操作。
例如,如果我们只需要查询 id 和 val 字段,我们可以执行以下语句:
1 | select id,val from test where val=4 limit 300000,5; |
这样,Mysql 只需要扫描索引页,而不需要访问数据页,提高了查询效率。
使用子查询。
如果我们不能使用索引覆盖扫描,或者查询字段较多,我们可以尝试使用子查询,也就是先用一个子查询找出我们需要的记录的 id 值,然后再用一个主查询根据 id 值获取其他字段。
例如,我们可以执行以下语句:
1 | select * from test where id in (select id from test where val=4 limit 300000,5); |
这样,Mysql 先执行子查询,在 val 索引上进行范围扫描,并返回 5 个 id 值。然后,Mysql 再执行主查询,在 id 索引上进行点查找,并返回所有字段。这样,Mysql 只需要扫描 5 个数据页,而不是 300005 个数据页,提高了查询效率。
2025.07.29
算法题
除了困难题,都刷完了,现在开始按顺序往下写题目!!!尽量不依赖编译器,直接在网站上写。
八股文
看来javaguide的mysql面试题,差不多3/4了,还是要多多理解,问问ai,看看更多的文章啊。
黑马头条项目复习
2025.07.31
前言:一个月马上就要过去了,我现在才发现自己是多么愚蠢,浪费了一个月的时间,对自己的提升少之又少。
我发现是我不够自律,没有严格按照行程来,或者说一开始我就忘记了遵守行程,每天除了学外就是玩,忽视掉了复盘每日收获,同时每天都很晚睡,对身体伤害很大。此外,不能再lu管了,这对我来说,代价太大了。
从现在开始,每天一定要按时复盘,周末总结整周收获,早睡早起,有个健康的身体比什么都重要,或者只有健康的身体,我才能高效学习。
工作日
- 8:30 起床:刷牙、洗脸、背八股
- 中间时间段自我查漏补缺
- 22:00 复盘今天收获
- 23:30 上床睡觉
周末
- 早睡早起
- 锻炼身体
- 总结一周学习内容
算法题
按顺序刷了之前写的算法
1 | 560. 和为 K 的子数组 解法:维护一个从索引0开始的子串map,每次遍历先判断是否有符合条件的字串,然后再加入map中 |
八股文
早上复习了之前记的jvm相关内容和看了恒生聚源的八股文,两个开放题,设计高考志愿填报算法思路(分析条件、记录高校和名额的Map,根据分数由高到低加入到优先队列,按志愿顺序循环遍历来实现)、两个支股票最长连续上升天数
黑马头条项目复习
完成了day09的所有接口、day10复习完成、day11还是KakfaStream待完成
2025.08.01
算法题
八股文
计算机网络学习
2025.08.04
焦虑×0,行动×100
快速学习了,
算法题
1 | 23. 合并 K 个升序链表 //使用优先队列 |
八股文
看了很多零散八股,不是说不好,是太多了,很多不是高频八股,我起码得先把高频八股摸头讲透再说,对吧。
计网之后接下来的学习内容:
- 线程池
- Redis面试题
- Spring源码
- Mybatis源码
- Linux
计网学习
小林coding网络基础篇
1.TCP/IP网络模型
分为4层
应用层:区分HTTP、HTTPS、SMTP等协议
传输层:携带端口。区分TCP和UDP协议
网络层:携带ip
网络接口层(数据链路层):携带MAC地址


2.键入网址到网页显示,到底发生了什么
真实地址查询–DNS
可靠传输–TCP
在http传输数据之前,需要建立tcp连接(双方计算机会维护状态位),tcp的连接称为三次握手。
三次握手主要是为了确保客户端和服务端的收发消息的能力没问题。
流程如下:
一开始,客户端和服务端都处于close状态。
然后,由客户端主动发起连接请求syn,发送之后,处于syn_sent状态。这是第一次握手,客户端请求建立连接
服务端收到发起的连接,ack客户端的syn,然后自己也发起请求连接syn。这是第二次握手,服务端确认客户端的连接请求,自己也请求建立连接
客户端接收到服务端的ack,处于established状态,因为一发一收成功了,然后发送ack来响应服务端的syn。这是第三次握手,客户端确认服务端的连接请求
服务端接收到客户端发来的ack后,也进入established状态。
此时三次握手结束。
描述还是看下面表格来吧。。。。。。
| 步骤 | 方向 | 报文内容 | 作用 | 状态变化 |
|---|---|---|---|---|
| 第一次握手 | 客户端 → 服务端 | SYN=1, seq=x | 客户端请求建立连接 | CLOSED → SYN_SENT |
| 第二次握手 | 服务端 → 客户端 | SYN=1, ACK=1, seq=y, ack=x+1 | 服务端确认并请求建立连接 | LISTEN → SYN_RCVD |
| 第三次握手 | 客户端 → 服务端 | ACK=1, seq=x+1, ack=y+1 | 客户端确认服务端的请求 | SYN_SENT → ESTABLISHED(客户端)SYN_RCVD → ESTABLISHED(服务端) |
2025.08.11
今天到08.15的任务目标如下:
1.线程池
2.spring源码八股
3.redis相关八股
4.项目复习,先头条后点评
5.实验室项目尽量试着包装吧
6.微服务相关知识复习(相关组件+RabbitMQ)
7.编写好简历
线程池
池化思想:字符串常量池、线程池、数据库连接池
主要目的就是提高资源的利用率,避免资源的频繁创建和释放
线程池优点
- 提高线程的利用率
- 提高程序的响应速度
- 便于统一管理线程对象
- 可以控制最大并发数
简易线程池创建
1 | public static void main(String[] args) { |
其中阻塞队列用于存放没有被执行的Runnable任务。
参数解释:
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:非核心线程的空闲的存活时间
unit:时间单位
workQueue:阻塞队列
threadFacotry:线程工厂(用于创建线程的)
handler:拒绝策略
拿ExecutorService threadPool = Executors.newFixedThreadPool(3);来说,它的底层其实就是使用ThreadPoolExecutor方法来创建的一个简易线程池

而LinkedBlockingQueue本身是一个无界队列,在高并发的情况下,这个队列可能非常长,而导致OOM。
1 | public LinkedBlockingQueue() { |
这也是为什么不推荐使用Executors自带的线程池创建方法来创建线程池的原因。
1 | //固定大小线程池,无界阻塞队列 |
线程池工作流程
以上面代码为例
- 来了新任务,先使用核心线程(core=3)执行
- 如果核心线程都在忙,就把任务放到阻塞队列中(容量=3)
- 如果阻塞队列也满了,才会创建额外的临时线程,直到达到最大的线程数(max=5)
- 如果最大线程数也满了,阻塞队列也满了的情况下,会触发拒绝策略
源码解析
execute和addwork源码
1 | public void execute(Runnable command) { |
线程池状态
ctl.get()的返回值,返回的高 3 位表示线程池状态,低 29 位表示工作线程数。
状态值(高3位):
- RUNNING: 111 (接受新任务,处理队列任务)
- SHUTDOWN: 000 (不接受新任务,但处理队列任务)
- STOP: 001 (不接受新任务,不处理队列任务,中断进行中的任务)
- TIDYING: 010 (过渡状态,所有任务终止)
- TERMINATED: 011 (完全终止)
Work类(选取了部分内容进行解释,更具体可以看源码)
1 | final Thread thread; |
线程移除
1.超过超时时间
在ThreadPoolExecutor的源码中,线程回收是通过Worker类和getTask()方法实现的:
Worker类:
- 每个工作线程都被封装为一个Worker对象
- Worker不断从阻塞队列中获取任务执行
getTask()方法:
1
1
2
3
4
5
6
7// 简化的Worker.run()逻辑
while (task != null || (task = getTask()) != null) {
// 执行任务
task.run();
task = null;
}
// 当getTask()返回null时,循环结束,线程退出关键判断逻辑:
timed = allowCoreThreadTimeOut || wc > corePoolSize- 如果允许核心线程超时(allowCoreThreadTimeOut = true,即所有线程都允许超时回收)) 或 当前线程数大于核心线程数,则该线程是”可超时的”
- 只有”可超时的”线程才会在获取任务时使用带超时的poll方法
当 wc > corePoolSize 时(即当前工作线程数超过了核心线程数):
- 所有线程(包括核心线程)都会被当作”可超时回收”的线程对待
- 每个线程在执行
getTask()时都会使用带超时的poll()方法 - 任何线程(包括最初创建的核心线程)如果空闲超过
keepAliveTime都可能被回收
关键点:
- 线程池不是按”创建顺序”区分核心/非核心线程
- 而是根据**当前总线程数(wc)与核心线程数(corePoolSize)**的实时比较结果
- 只要
wc > corePoolSize,所有线程都变成”可回收”状态
总结:当线程池中线程总数大于核心线程数或者允许核心线程超时回收时,所有线程都会变成可回收状态,该状态下,只要有线程超过空闲阈值就会通过CAS进行回收。
CAS回收的设计优势包括:
- 线程安全:CAS操作保证了线程计数修改的原子性
- 避免竞争:即使多个线程同时尝试回收,也只会成功一个
- 优雅重试:失败的线程可以重新评估回收条件
- 资源保护:避免过度回收或回收不足的问题
2.抛出未捕获异常
当线程调用worker的run方法时,如果线程处理处理任务发生异常没被捕获情况下,突然退出的标记属性completedAbruptly就为true,然后执行finally中的方法来创建一个新的Worker对象加入到线程池当中。
run方法源码
1 | final void runWorker(Worker w) { |
processWorkerExit源码
1 | private void processWorkerExit(Worker w, boolean completedAbruptly) { |
总结
结合源码的工作流程
- 任务提交(
execute):- 先检查线程数是否 <
corePoolSize,是则调用addWorker(command, true)创建核心线程直接执行; - 否则尝试入队,若队列已满且线程数 <
maximumPoolSize,则addWorker(command, false)创建临时线程; - 若队列和线程数均满,触发拒绝策略。
- 先检查线程数是否 <
- 线程复用(
runWorker循环):- 每个Worker启动后进入
while (task != null || (task = getTask()) != null)循环,优先执行firstTask,后续通过getTask()从队列拉取任务; getTask()根据keepAliveTime决定阻塞/超时等待,非核心线程超时后返回null,线程终止。
- 每个Worker启动后进入
- 底层控制(
ctl状态机 +mainLock):- 高3位记录状态(RUNNING/SHUTDOWN/STOP等),低29位统计线程数,通过CAS保证原子性;
mainLock保护workers集合,确保扩容/缩容时线程安全。
线程池对象调用execute() -> 内部调用addWorker()创建线程 -> 线程内部通过while循环执行任务
一口气解释Worker对象:
ThreadPoolExecutor相当于线程调度的主体,它内部维护了关于Worker的HashSet对象workers,并定义了Worker内部类,它实现了Runable接口,Worker这个类中主要包含了两个属性firstTask和thread,由源码可知,worker只有一个传入runable接口实现类的构造方法,通过这个构造方法,把传入的任务赋值给fistTask,然后以当前Worker对象为value创建一个线程赋值给thread属性(相当于我拥有调用我的线程),firstTask是线程在创建并启动后第一个执行的任务,后续任务都会通过getTask()方法来从阻塞队列中获取。所以在java中的线程池其实就是通过Worker的hashset对象workers来维护的。
一口气解释ThreadPoolExecutor核心组件:
ThreadPoolExecutor作为线程调度中枢,通过五大核心属性控制线程池行为:
- corePoolSize:核心线程数,常驻内存的线程数量,默认不会回收(除非allowCoreThreadTimeOut=true)
- maximumPoolSize:线程池最大容量=核心线程+临时线程(临时线程空闲超时后被回收)
- workQueue:任务缓存队列(ArrayBlockingQueue有界队列/LinkedBlockingQueue无界队列等)
- keepAliveTime:临时线程空闲存活时间(默认60秒,超时后触发回收)
- threadFactory:线程工厂(可定制线程命名/优先级/守护状态等)
一口气解释ctl原子变量:
ctl是AtomicInteger类型,高3位存储线程池状态
- RUNNING: 111 (接受新任务,处理队列任务)
- SHUTDOWN: 000 (不接受新任务,但处理队列任务)
- STOP: 001 (不接受新任务,不处理队列任务,中断进行中的任务)
- TIDYING: 010 (过渡状态,所有任务终止)
- TERMINATED: 011 (完全终止)
低29位记录工作线程数,通过CAS操作保证原子性更新,是线程池状态控制的核心枢纽。
一口气解释拒绝策略:
当线程数达max且队列满时会触发RejectedExecutionHandler,内置四种策略:
- AbortPolicy(默认):直接抛出RejectedExecutionException
- CallerRunsPolicy:让提交任务的线程自己执行
- DiscardPolicy:静默丢弃新任务
- DiscardOldestPolicy:丢弃队列头部的老任务后重试
一口气解释任务执行流程:
任务提交后经历三级缓冲:先尝试创建核心线程执行(addWorker(command, true)),失败后入队(workQueue.offer(command)),队列满则创建临时线程(addWorker(command, false)),全都失败则触发拒绝策略,已分配的任务由Worker线程通过runWorker()循环执行,先处理firstTask再通过getTask()从队列获取新任务实现复用。
收获挺大的,对线程池的实现原理有了较为深入清晰的认识了!!!开心
2025.08.12
1.JDK、JRE、JVM区别
JVM,就是java虚拟机,它主要负责加载字节码文件到内存中并执行,jvm屏蔽了操作系统之间差异,是java跨平台的核心,可以让同一份字节码文件可以在不同的操作系统上正常运行。
JRE (Java Runtime Environment),指的是java运行时环境,它包含了jvm和核心类库(lib),核心类库主要就是提供基础功能(比如字符串处理、文件操作这些),如果没有这些类库,java程序就无法直接调用系统级的功能。
JDK(Java Development Kit),是java开发工具包,它包含了jre和开发工具(如编译器javac,负责将java源码编译成字节码文件、调试器jdb、打包工具jar等)。有了jdk,我们就可以开发java程序。
如果只需运行java程序,大部分场景下只需要安装jre就可以了。但如果我们需要开发java程序,就一定需要安装jdk
如果使用JSP部署Web应用程序,只需要运行java程序即可,但由于JSP转换成的servlet源码需要编译成字节码文件,所以需要JDK携带的javac编译器来编译servlet。
JDK vs JRE:关键区别
| 功能 | JDK | JRE |
|---|---|---|
编译 Java 源码(javac) |
✅ | ❌ |
运行字节码(java) |
✅ | ✅ |
| 包含 JVM | ✅ | ✅ |
| 包含核心类库(Java API) | ✅ | ✅ |
| 开发工具(调试、打包等) | ✅ | ❌ |
2025.08.18
每日八股
1.Exception和Error的区别
都是Throwable的子类。
Exception:程序可以处理的异常,可以通过try-catch进行捕获。
Error:程序不可处理的错误,不建议使用try-catch进行捕获。一般指的是虚拟机方面导致的问题,比如jvm运行出错、内存溢出OOM就属于Error、方法调用溢栈等就属于Error。
Exception还可以分为两大类CheckedException(受检查异常,编译期间必须显式进行try-catch,比如平时写代码时,编译器提示必须要捕获或者抛出异常,比如IO异常)和UnCheckedException(指的是不受检查异常,这种属于运行时异常,不进行显示try-catch也可以通过编译)
RuntimeException 及其子类都统称为非受检查异常,常见的有):
NullPointerException(空指针错误)IllegalArgumentException(参数错误比如方法入参类型错误)NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)ArrayIndexOutOfBoundsException(数组越界错误)ClassCastException(类型转换错误)ArithmeticException(算术错误)SecurityException(安全错误比如权限不够)UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
2.SPI和API的区别

SPI(服务提供者的接口),指的是提供给服务提供者或者扩展框架功能的开发者去实现的接口。SPI将服务提供者和服务接口分离开来,由服务调用者来定义相应的接口,从而实现服务调用方和实现方的解耦,当需要修改或者替换服务的时候,就不要去修改调用方了。
SPI和API的区别主要就是这个服务接口是由谁来提供的。在API中,是由实现方来定义和实现的,调用方需要根据实现的接口来选择性的选取所需要调用的方法。而SPI机制下,是由调用方来指定自己所需的接口,让实现去相应的接口。
SPI机制的好处就是大大提高了接口实现的灵活性。
缺点就是就是无法实现按需加载,以及可能会出现并发问题
ServiceLoader 在加载接口实现时,会一次性加载并实例化所有在 META-INF/services 中声明的实现类,即使只使用其中一个,也会全部加载,造成资源浪费。
当多个 ServiceLoader 同时 load 时,会有并发问题。
2025.08.20
java设计模式
断更了,在这里记得已经开始找实习了。
