1.什么情况用乐观锁,什么情况用悲观锁
乐观锁适用场景
-
读多写少:事务间以读取为主,冲突概率低(如博客阅读量统计)。
-
高并发写入但冲突较少:例如用户积分更新(多数操作不会冲突)。
-
长事务场景:事务执行时间长,但冲突概率低(如订单状态流转)。
-
实现方式:
- 数据库版本号(
version字段)或时间戳。 - CAS(Compare And Swap)操作(如 Java 的
AtomicInteger)。
- 数据库版本号(
典型场景举例:
- 电商系统中的商品库存扣减(若库存冲突少,可用乐观锁)。
- 社交媒体的点赞数更新(冲突概率低)。
悲观锁适用场景
-
写多读少:事务间频繁修改同一数据(如银行转账)。
-
高冲突场景:数据竞争激烈(如限量秒杀库存)。
-
强一致性要求:必须保证数据操作的原子性,不允许脏读或覆盖。
-
实现方式:
- 数据库行锁(
SELECT ... FOR UPDATE)。 - 分布式锁(如 Redis 的
SETNX或 RedLock)。
- 数据库行锁(
典型场景举例:
- 支付系统的账户余额扣减(需严格避免超扣)。
- 库存秒杀场景(高并发下需强制排他)。
乐观锁:适合冲突少、追求高并发的场景,通过版本号或CAS实现,简单高效。
悲观锁:适合冲突多、强一致性的场景,通过排他锁避免竞争,但需谨慎处理死锁。
2.redis事务
Redis事务通过一组命令实现批量操作的原子性执行,但其事务模型与传统关系型数据库(如MySQL)有显著差异
-
MULTI
-
标记事务的开始,后续命令会被放入队列,直到执行
EXEC。 -
示例:
MULTI SET key1 "value1" INCR key2 EXEC
-
-
EXEC
- 执行事务队列中的所有命令,按顺序串行化执行。
- 特性:即使某条命令报错(如类型错误),其他命令仍会执行,不支持回滚。
-
DISCARD
- 放弃事务队列中的所有命令,清空事务。
-
WATCH key [key ...]
- 乐观锁机制:监视一个或多个键,若在事务执行前这些键被修改,事务会被取消(
EXEC返回nil)。 - 用途:实现基本的并发控制,避免数据竞争。
- 乐观锁机制:监视一个或多个键,若在事务执行前这些键被修改,事务会被取消(
Redis事务的特性
| 特性 | 说明 |
|---|---|
| 原子性 | 事务内的命令会按顺序串行执行,不会被其他客户端命令打断。 |
| 错误处理 | - 语法错误:若命令存在语法错误(如参数错误),整个事务不执行。 |
- 运行时错误:如对字符串类型执行INCR,仅该命令失败,其他命令继续执行。 | |
| 不支持回滚 | Redis事务没有回滚机制,即使命令执行失败,事务仍会继续执行后续命令。 |
| 轻量级 | 事务的本质是命令队列,无锁机制,性能开销低。 |
| 特性 | Redis事务 | 关系型数据库事务(如MySQL) |
|---|---|---|
| 原子性 | 所有命令按顺序执行,但部分失败不影响整体 | 所有操作要么全部成功,要么全部回滚 |
| 隔离性 | 无隔离级别,依赖单线程模型保证串行化 | 支持多种隔离级别(如读未提交、可重复读) |
| 回滚 | 不支持 | 支持 |
| 适用场景 | 批量操作、简单并发控制 | 复杂事务、强一致性需求 |
Redis事务通过MULTI/EXEC实现批量命令的原子化执行,适合简单操作和轻量级并发控制,但不支持回滚和复杂隔离级别。对于强一致性需求或复杂逻辑,建议使用Lua脚本替代事务。理解其特性与局限性,可避免误用场景(如金融交易)。
3.redis定时消息队列
Redisson 的 RDelayedQueue
当我们要添加一个数据到延迟队列的时候,redission会把数据+超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用。
我们用RDelayedQueue的offer方法将订单添加到了延迟队列,并指定了延迟时间,当元素的延迟时间到达时,Redission会将元素从RDelayedQueue转移到与之关联的RBlockingDeque。
然后在检查是否要关单的时候,另起了一个线程,不断循环读取到期的订单。值得注意的是 take方法从RBlockingDeque中获取元素,这是一个阻塞操作,如果没有元素,它会一直等到,直到有元素。
4.g1底层原理
G1垃圾回收器(Garbage-First)的底层原理
G1(Garbage-First)是Java HotSpot虚拟机中面向服务端应用的垃圾回收器,设计目标是在可控的停顿时间和高吞吐量之间取得平衡。其核心思想是通过分区管理和并发标记,优先回收垃圾比例高的区域(Region),从而优化内存管理效率。以下是其底层原理的详细解析:
一、核心设计思想
-
分代与分区结合
-
分代模型:保留年轻代(Young Generation)和老年代(Old Generation)的概念,但物理上不再连续。
-
分区(Region)管理:
- 堆内存被划分为多个大小相等的独立区域(Region),默认大小为 2MB~32MB(可配置)。
- 每个Region可以是 Eden区、Survivor区、Old区或Humongous区(存放超大对象)。
- 优势:动态调整Region用途,减少内存碎片。
-
-
并发标记与优先回收
- 并发标记阶段:与应用程序线程并行,标记存活对象并统计各Region的垃圾比例。
- 混合回收(Mixed GC):在回收年轻代的同时,选择部分老年代Region(垃圾比例高的)进行回收,避免Full GC。
二、关键数据结构与技术
-
Region与内存布局
-
Region大小:默认由堆总大小自动计算(2MB~32MB),需为2的幂次方。
-
Region类型:
- Eden:新对象分配区域。
- Survivor:存活对象迁移区域(From/To)。
- Old:长期存活对象存储区域。
- Humongous:存放超过Region 50%大小的对象,避免跨Region复制。
-
-
Remembered Set(RSet)
- 作用:每个Region维护一个RSet,记录其他Region引用本Region中对象的指针。
- 优势:年轻代回收时无需扫描整个老年代,仅需检查RSet中的引用,提升效率。
-
SATB(Snapshot-At-The-Beginning)算法
- 目的:在并发标记阶段记录初始对象快照,确保标记的准确性。
- 实现:通过写屏障(Write Barrier)捕获对象引用变化,记录新增或修改的引用。
三、工作流程
-
年轻代回收(Minor GC)
-
触发条件:Eden区满时触发。
-
步骤:
- 初始标记(STW):标记GC Roots直接关联的对象。
- 根区域扫描:扫描Survivor区引用的对象。
- 并发标记:遍历整个堆,标记存活对象。
- 最终标记(STW):处理并发标记阶段的变更。
- 清理:统计各Region存活对象比例,优先回收垃圾比例高的Region。
-
-
并发标记周期
- 全局并发标记:全堆扫描,确定各Region的垃圾比例。
- 混合回收(Mixed GC):回收年轻代Region + 部分老年代Region(基于垃圾比例)。
-
Humongous对象处理
- 超大对象直接分配到Humongous区,只能通过Full GC回收,需合理设置Region大小以减少此类对象的影响。
四、核心优势与性能优化
-
可预测的停顿时间
- 通过
-XX:MaxGCPauseMillis参数设定目标最大停顿时间(默认200ms),G1动态调整每次回收的Region数量以满足目标。
- 通过
-
高吞吐量
- 并发标记与应用程序线程并行,减少停顿对业务的影响。
-
内存碎片控制
- 分区管理机制避免传统分代模型中的内存碎片问题。
五、与其他垃圾回收器的对比
| 特性 | G1 | CMS | Parallel GC |
|---|---|---|---|
| 目标 | 低停顿 + 高吞吐 | 低停顿 | 高吞吐 |
| 内存布局 | 分区(Region) | 连续分代空间 | 连续分代空间 |
| 碎片处理 | 较少(Region机制) | 易产生碎片 | 无碎片 |
| 停顿预测 | 支持 | 不支持 | 不支持 |
| 适用场景 | 大内存、低延迟 | 中小内存、低延迟 | 高吞吐、后台计算 |
六、调优参数与实践建议
-
关键参数
-XX:+UseG1GC:启用G1。-XX:MaxGCPauseMillis:目标最大停顿时间(默认200ms)。-XX:G1HeapRegionSize:Region大小(默认自动计算)。-XX:InitiatingHeapOccupancyPercent:触发并发标记的老年代占用阈值(默认45%)。
-
调优建议
- 堆大小:根据应用需求调整,避免频繁Full GC。
- Region大小:根据对象分布选择(大对象多则增大Region)。
- 监控指标:关注
gc pauses、young gc count和mixed gc count。
七、总结
G1通过分区管理、并发标记和混合回收机制,在保证可预测停顿时间的同时,兼顾高吞吐量。其核心优势在于:
- 动态调整:根据垃圾比例优先回收高价值区域。
- 高效内存利用:减少碎片,支持超大堆内存。
- 低延迟:通过SATB和RSet优化标记与回收效率。
适用场景:大内存(如几十GB)、对延迟敏感的应用(如Web服务、实时系统)。理解其原理有助于合理配置参数,避免性能瓶颈。
5.AtomicInteger底层原理
AtomicInteger 是 Java 提供的一种原子操作类,位于 java.util.concurrent.atomic 包中,主要用于提供在多线程环境下对整数值进行原子更新的操作。它通过底层的硬件支持(如 CAS 操作)来确保线程安全,而不需要使用传统的锁机制(如 synchronized 或 ReentrantLock)。
1. AtomicInteger 的基本作用
AtomicInteger 提供了对整数的原子操作,它的主要方法包括:
get():获取当前值。set(int newValue):设置新值。getAndSet(int newValue):获取当前值并设置为新值。incrementAndGet():将当前值加 1 后返回新值。addAndGet(int):将当前值加上指定增量后返回新值。compareAndSet(int expect, int update):如果当前值等于预期值,则将其设置为新的值,返回操作是否成功。
这些方法保证了在多线程环境下对 AtomicInteger 的操作是线程安全的,避免了传统的同步锁机制所带来的性能开销。
2. AtomicInteger 底层原理
AtomicInteger 的底层实现主要依赖于 CAS (Compare-And-Swap) 操作,这是硬件原语提供的一种原子操作,用于实现无锁并发控制。CAS 操作的本质是:
- 比较内存中的值是否等于预期值。
- 如果相等,则更新为新的值。
- 如果不等,则不做任何修改,并返回失败信息。
CAS 操作是原子性的,它通过底层的硬件指令直接支持,通常是 CPU 中的 CMPXCHG 指令,能够保证操作的原子性。
2.1 CAS 操作的流程
AtomicInteger 中的值实际上是通过 volatile 变量来存储的。例如,在 AtomicInteger 的内部,有一个 volatile int value 变量来存储实际的整数值。CAS 操作会使用该 value 变量来进行比较和交换,操作流程如下:
- 读取当前值:获取
value当前值。 - 比较当前值和期望值:如果当前值和期望值相同,则可以安全地更新为新的值。
- 更新值:通过 CAS 操作原子地将
value从期望值更新为新的值。 - 失败重试:如果 CAS 操作失败(即当前值与期望值不相等),说明其他线程已经修改了
value,需要重新读取并尝试更新,直到成功。
2.2 CAS 操作的实现
AtomicInteger 中的核心操作通常是通过 sun.misc.Unsafe 类来实现的。Unsafe 是 Java 中一个较为底层的类,提供了操作内存和执行 CAS 等低级操作的方法。通过 Unsafe,AtomicInteger 可以执行类似 compareAndSwapInt 这样的底层 CAS 操作。
3. 性能优势
相比传统的锁机制,CAS 操作不需要阻塞线程,而是通过不断地尝试更新值来实现线程安全。这种无锁操作大大提高了并发性能,特别是在高并发的情况下,CAS 操作能够减少上下文切换和锁竞争,从而提供更好的性能。
3.1 自旋等待(Spin Wait)
CAS 操作可能会失败,尤其是在高并发的情况下。如果 CAS 操作失败,线程通常会自旋等待,直到获取到更新的权限。这种自旋等待通常不会引入较大的性能开销,特别是当操作失败的次数较少时。
3.2 ABA 问题
CAS 操作存在一个常见的问题,叫做 ABA 问题。即假设一个变量从值 A 变成 B,然后再变回 A,CAS 操作就无法区分该变量是否发生了实际的改变。在多线程高并发的环境中,ABA 问题可能导致错误的判断。为了解决这个问题,Java 提供了 AtomicStampedReference 和 AtomicMarkableReference 类,它们引入了版本号或标记,来避免 ABA 问题。
4. 总结
AtomicInteger 是 Java 中提供的一种高效的原子操作类,它通过底层的 CAS 操作来保证线程安全。相比传统的锁机制,AtomicInteger 提供了更高效的并发控制,特别是在高并发环境下。它的底层依赖于硬件支持的原子操作(如 CMPXCHG 指令),并通过 volatile 关键字确保内存可见性。尽管 CAS 操作性能优秀,但它也存在一些问题,如 ABA 问题,Java 通过引入一些其他类来解决这些问题。
6.mysql事务回滚原理
MySQL的事务回滚机制依赖于其存储引擎InnoDB的undo log(撤销日志),通过记录数据修改前的状态来实现回滚操作。以下是其核心原理和流程的详细解析:
一、事务回滚的核心机制
1. Undo Log的作用
- 记录修改前的数据:当事务对数据进行修改(INSERT/UPDATE/DELETE)时,InnoDB会将修改前的数据记录到undo log中。
- 支持回滚与MVCC:undo log不仅用于事务回滚,还为多版本并发控制(MVCC)提供历史数据版本,实现非锁定读。
2. 回滚的触发场景
- 显式回滚:用户执行
ROLLBACK命令。 - 隐式回滚:事务执行过程中遇到错误(如主键冲突、死锁、连接断开等)。
二、Undo Log的数据结构
1. 存储位置
- 独立表空间:MySQL 5.6+支持将undo log存储在独立的
undo_表空间中(默认共享表空间ibdata1)。 - 文件格式:每个undo log记录包含表空间ID、页号、事务ID及修改前的数据。
2. 记录类型
- INSERT操作:记录插入的数据行,回滚时删除对应行。
- DELETE操作:记录被删除的数据行,回滚时重新插入。
- UPDATE操作:记录更新前的旧值,回滚时恢复旧值。
三、事务回滚的实现步骤
-
记录Undo Log:
在事务执行修改操作时,InnoDB会先将原始数据写入undo log,并通过trx_id(事务ID)和roll_ptr(回滚指针)关联到数据行。 -
修改数据:
更新数据页(Buffer Pool中的内存页),并将修改异步刷新到磁盘(通过redo log保证持久性)。 -
回滚操作:
-
当需要回滚时,InnoDB按逆序执行undo log中的操作:
- INSERT → 删除插入的数据。
- DELETE → 重新插入被删数据。
- UPDATE → 用旧值覆盖当前值。
-
清理undo log:提交的事务对应的undo log会被标记为可清理,由purge线程异步回收。
-
四、事务回滚与持久性
1. Undo Log的持久化
- 写入redo log:undo log的修改会先记录到redo log,确保即使系统崩溃,undo log的内容也不会丢失。
- 刷盘策略:由
innodb_flush_log_at_trx_commit参数控制(默认1,即每次提交时刷盘)。
2. 回滚的原子性保证
-
事务的原子性通过undo log和redo log共同实现:
- undo log:提供回滚所需的数据快照。
- redo log:保证已提交事务的修改持久化。
五、事务回滚的性能影响
1. 回滚耗时因素
- 事务大小:涉及大量数据修改的事务回滚时间更长。
- 锁竞争:回滚时需持有相关数据行的排他锁,可能阻塞其他事务。
2. 优化建议
- 缩短事务:避免长时间运行的操作,减少事务持有锁的时间。
- 批量操作:分批次提交事务,降低单次回滚的开销。
- 合理设计索引:减少全表扫描,提升回滚效率。
六、与Redo Log的协作
| 日志类型 | 作用 | 回滚中的角色 |
|---|---|---|
| Redo Log | 保证已提交事务的持久性 | 不直接参与回滚,但确保undo log的持久化 |
| Undo Log | 支持回滚和MVCC | 提供数据修改前的状态,用于逆向恢复 |
七、示例场景
1. 事务回滚流程
sql
复制
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 修改前数据写入undo log
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 修改前数据写入undo log
-- 发生错误,执行ROLLBACK
ROLLBACK;
-
回滚过程:
- 根据undo log恢复id=1的balance(+100)。
- 根据undo log恢复id=2的balance(-100)。
2. 提交后的事务
- 已提交事务的undo log可能被purge线程清理,无法回滚。
八、总结
MySQL的事务回滚通过undo log记录数据修改前的状态,结合redo log的持久化保证,实现了ACID中的原子性和一致性。其核心流程如下:
- 记录:修改数据前写入undo log。
- 修改:更新数据页并记录redo log。
- 回滚:按逆序应用undo log恢复数据。
最佳实践:
- 避免大事务,减少回滚风险。
- 合理配置
innodb_undo_logs和innodb_purge_threads以优化性能。 - 监控
trx_rseg_history_len,防止undo log膨胀。
7.spring三级缓存
Spring 的三级缓存机制主要用于解决单例 Bean 的循环依赖问题。其核心是通过分层缓存提前暴露 Bean 的引用,确保 Bean 在未完全初始化时仍能被其他 Bean 依赖注入。以下是三级缓存的详细解析:
一、三级缓存的结构
| 缓存层级 | 作用 | 存储内容 |
|---|---|---|
| singletonObjects | 存放完全初始化的单例 Bean | 完整且初始化完成的 Bean 实例 |
| earlySingletonObjects | 存放提前暴露的 Bean(尚未完全初始化) | 未完成初始化的 Bean 引用 |
| singletonFactories | 存放 Bean 的工厂对象,用于生成早期引用 | ObjectFactory(生成 Bean 的工厂) |
二、循环依赖的解决流程
假设有两个 Bean:BeanA 依赖 BeanB,BeanB 又依赖 BeanA。
-
创建 BeanA
- Spring 容器开始创建
BeanA,实例化后生成ObjectFactory并放入singletonFactories。 - 填充
BeanA的属性时发现依赖BeanB,于是开始创建BeanB。
- Spring 容器开始创建
-
创建 BeanB
-
实例化
BeanB,生成其ObjectFactory放入singletonFactories。 -
填充
BeanB的属性时发现依赖BeanA,此时检查singletonObjects:BeanA不在singletonObjects(尚未完全初始化)。- 检查
earlySingletonObjects,若无则从singletonFactories获取BeanA的工厂,生成一个 早期引用 并放入earlySingletonObjects。
-
-
完成 BeanB 的初始化
BeanB使用早期引用的BeanA完成自身初始化,并存入singletonObjects。
-
完成 BeanA 的初始化
BeanA获取到完全初始化的BeanB,继续完成自身初始化,并存入singletonObjects。
三、关键代码逻辑
java
复制
// 伪代码:创建 Bean 的流程
public Object getSingleton(String beanName) {
// 1. 检查 singletonObjects(完全初始化的 Bean)
Object singleton = singletonObjects.get(beanName);
if (singleton != null) return singleton;
// 2. 检查 earlySingletonObjects(提前暴露的 Bean)
singleton = earlySingletonObjects.get(beanName);
if (singleton != null) return singleton;
// 3. 从 singletonFactories 获取工厂,生成早期引用
ObjectFactory<?> factory = singletonFactories.get(beanName);
if (factory != null) {
singleton = factory.getObject(); // 生成早期引用
earlySingletonObjects.put(beanName, singleton);
singletonFactories.remove(beanName);
return singleton;
}
return null;
}
四、三级缓存的协作
-
singletonFactories:
- 在 Bean 实例化后,立即将
ObjectFactory存入此缓存,用于后续生成早期引用。 - 用途:允许其他 Bean 在依赖注入时获取未完全初始化的 Bean。
- 在 Bean 实例化后,立即将
-
earlySingletonObjects:
- 当其他 Bean 请求依赖时,若目标 Bean 正在创建,则从
singletonFactories生成早期引用并存入此缓存。 - 用途:缓存早期引用,避免重复生成。
- 当其他 Bean 请求依赖时,若目标 Bean 正在创建,则从
-
singletonObjects:
- Bean 完全初始化后,从
earlySingletonObjects移动至此缓存,供后续直接使用。
- Bean 完全初始化后,从
五、限制与注意事项
-
仅支持单例 Bean
- 原型(Prototype)Bean 不会被缓存,每次请求都生成新实例,无法解决循环依赖。
-
构造方法循环依赖无法解决
- 若循环依赖发生在构造方法中(如
BeanA的构造参数需要BeanB,反之亦然),三级缓存无法处理,会抛出BeanCurrentlyInCreationException。
- 若循环依赖发生在构造方法中(如
-
代理 Bean 的影响
- 若 Bean 被 AOP 代理,早期引用可能是代理对象而非原始对象。
六、总结
Spring 的三级缓存通过 提前暴露 Bean 引用 解决单例 Bean 的循环依赖问题:
- singletonFactories:生成早期引用的工厂。
- earlySingletonObjects:缓存未完全初始化的 Bean。
- singletonObjects:存放完全初始化的 Bean。
核心价值:
- 允许 Bean 在未完全初始化时被其他 Bean 依赖注入。
- 避免因循环依赖导致的死锁或初始化失败。
最佳实践:
- 尽量避免循环依赖,优先通过 Setter 方法注入。
- 构造方法依赖需通过
@Lazy延迟加载解决。 - 理解其局限性,避免在复杂场景中滥用。
8.spring aop
Spring AOP 核心原理与实现
Spring AOP(Aspect-Oriented Programming)是一种基于代理的切面编程框架,用于实现横切关注点(如日志、事务、权限)的模块化。其核心是通过动态代理技术,在不修改原有代码的情况下增强目标对象的行为。以下是其核心原理和关键机制的详细解析:
一、AOP 核心概念
| 概念 | 说明 |
|---|---|
| 切面(Aspect) | 模块化的横切关注点(如日志记录、事务管理),通过注解或XML配置定义。 |
| 连接点(Join Point) | 程序执行中的特定点(如方法调用、异常抛出),Spring AOP仅支持方法级别的连接点。 |
| 通知(Advice) | 切面在连接点执行的动作,包括前置(@Before)、后置(@After)、环绕(@Around)等。 |
| 切入点(Pointcut) | 通过表达式匹配需要增强的连接点(如 execution(* com.example.service.*.*(..)))。 |
| 目标对象(Target) | 被切面增强的原始对象。 |
| 代理(Proxy) | Spring AOP 通过动态代理生成的目标对象的增强版本,分为 JDK 动态代理和 CGLIB 代理。 |
二、AOP 实现原理
Spring AOP 的核心是 动态代理,其实现方式取决于目标对象是否实现接口:
-
JDK 动态代理
-
条件:目标对象实现了接口。
-
原理:基于
java.lang.reflect.Proxy生成代理类,代理类实现相同接口,并通过InvocationHandler拦截方法调用。 -
代码示例:
java 复制 public interface UserService { void addUser(); } public class UserServiceImpl implements UserService { @Override public void addUser() { /* ... */ } } // 代理生成 UserService proxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class<?>[]{UserService.class}, (proxyObj, method, args) -> { // 前置增强 System.out.println("Before method"); // 调用目标方法 return method.invoke(new UserServiceImpl(), args); } );
-
-
CGLIB 代理
-
条件:目标对象未实现接口。
-
原理:通过继承目标类生成子类代理,覆盖父类方法并添加增强逻辑。
-
依赖:需引入
spring-core中的 CGLIB 库。 -
代码示例:
java 复制 public class UserService { public void addUser() { /* ... */ } } // CGLIB 代理生成 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 前置增强 System.out.println("Before method"); return proxy.invokeSuper(obj, args); } }); UserService proxy = (UserService) enhancer.create();
-
三、AOP 核心流程
- 切面定义:通过
@Aspect注解定义切面类,使用@Pointcut指定切入点表达式。 - 代理创建:Spring 容器在初始化 Bean 时,检查是否需要创建代理(基于切面匹配的 Bean)。
- 方法拦截:当调用目标方法时,代理对象拦截请求,按顺序执行通知(Advice)。
- 增强逻辑:在连接点前后或环绕执行切面代码(如日志、事务管理)。
四、通知类型(Advice)
| 通知类型 | 执行时机 | 代码示例 |
|---|---|---|
@Before | 方法执行前 | System.out.println("Before method"); |
@AfterReturning | 方法正常返回后 | 记录返回值或清理资源 |
@AfterThrowing | 方法抛出异常后 | 记录异常信息 |
@After | 方法执行后(无论是否成功) | 资源释放 |
@Around | 包裹目标方法,可控制执行流程 | 支持手动调用 ProceedingJoinPoint |
环绕通知示例:
java
复制
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method");
try {
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("After method");
return result;
} catch (Exception e) {
System.out.println("Exception handled");
throw e;
}
}
五、切入点表达式
通过 @Pointcut 定义切入点,常用语法:
-
execution:匹配方法签名(最常用)。
java 复制 @Pointcut("execution(* com.example.service.*.*(..))") // 匹配 service 包下所有方法 public void serviceMethods() {} -
within:匹配类或包。
java 复制 @Pointcut("within(com.example.service.*)") // 匹配 service 包下所有类 -
args:匹配方法参数类型。
java 复制 @Pointcut("args(String)") // 匹配参数为 String 类型的方法
六、AOP 与事务管理
Spring 的声明式事务(@Transactional)本质是通过 AOP 实现:
- 切面:
TransactionAspectSupport类定义事务切面。 - 通知:
@Around通知包裹目标方法,开启、提交或回滚事务。 - 代理:目标 Bean 被代理后,事务逻辑透明注入。
七、AOP 的局限性与适用场景
| 局限性 | 说明 |
|---|---|
| 仅支持方法级别的增强 | 无法直接拦截字段访问或构造方法。 |
| 基于动态代理 | 对 final 类或方法无法生成代理(CGLIB 无法继承 final 类)。 |
| 性能开销 | 频繁的方法拦截可能影响性能(可通过优化切入点表达式减少匹配范围)。 |
适用场景:
- 日志记录、性能监控、权限校验。
- 事务管理、缓存控制。
- 解耦横切关注点,提升代码复用性。
八、总结
Spring AOP 通过 动态代理 和 切面编程模型,实现了横切关注点的模块化:
- 核心机制:JDK/CGLIB 动态代理生成增强对象。
- 灵活性:通过切入点表达式精准匹配目标方法。
- 解耦:分离业务逻辑与横切逻辑,提升代码可维护性。
最佳实践:
- 优先使用
@Around处理复杂流程(如事务)。 - 避免过度使用 AOP,防止代码可读性下降。
- 结合
@Order注解控制多个切面的执行顺序。
9.spring循环依赖
在 Spring 中,循环依赖(Circular Dependency)指的是两个或多个 Bean 之间相互依赖的情况。例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,这就形成了一个循环依赖问题。
Spring 通过 三级缓存机制(或者说 单例模式下的循环依赖解决方案)来解决这种问题。我们可以理解为:Spring 容器会在实例化 Bean 的过程中,在实例化阶段用一个容器存储 Bean 实例,避免在构造函数阶段出现死锁。通过这种机制,Spring 可以在容器中创建这些依赖并成功注入。
问题描述
假设我们有两个类,A 和 B,其中 A 依赖于 B,而 B 又依赖于 A,如下:
javaCopy Code
@Component
public class A {
private B b;
@Autowired
public A(B b) {
this.b = b;
}
// getter and setter
}
@Component
public class B {
private A a;
@Autowired
public B(A a) {
this.a = a;
}
// getter and setter
}
如果 Spring 容器遇到这种情况,它会抛出 BeanCurrentlyInCreationException 异常,提示存在循环依赖。
Spring 解决循环依赖的原理
Spring 容器会使用 三级缓存机制来解决循环依赖问题,具体机制如下:
- 一级缓存(Singleton Objects Cache) :这是 Spring 容器的常规缓存,存放已经创建好的 Bean 实例(最终注入依赖完成的实例)。
- 二级缓存(Early Singleton Objects Cache) :当 Spring 开始创建 Bean 实例时,容器会先将正在创建的 Bean(处于中间状态的实例)放入二级缓存。
- 三级缓存(Singleton Factory Beans Cache) :用于保存通过工厂方法(
ObjectFactory或Supplier)生成的 Bean。
Spring 通过这三级缓存来解决循环依赖问题。具体流程是这样的:
- Spring 容器在创建
Bean A时,发现它依赖Bean B,于是进入Bean B的创建过程。 - 在创建
Bean B时,Spring 发现它依赖Bean A,此时Bean A尚未完全创建好,Spring 将Bean A放入二级缓存中,并继续创建Bean B。 Bean B创建完成后,Bean A继续从二级缓存中获取,Spring 再把Bean A填充完整,最后将Bean A和Bean B完成注入,返回给客户端。
解决循环依赖的关键
通过 构造器注入 的方式存在循环依赖问题,因为 Spring 容器无法在 Bean 实例化之前进行依赖注入。而 Setter 注入 或 字段注入 就不会出现问题,因为 Spring 会在实例化 Bean 后再进行依赖注入操作。
代码示例:解决循环依赖
- 构造器注入: 在构造器中直接进行依赖注入,存在循环依赖的风险。
javaCopy Code
@Component
public class A {
private B b;
@Autowired
public A(B b) {
this.b = b;
}
// getter and setter
}
@Component
public class B {
private A a;
@Autowired
public B(A a) {
this.a = a;
}
// getter and setter
}
- Setter 注入: 使用 Setter 方法注入依赖,解决了循环依赖问题。
javaCopy Code
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}
在 Setter 注入的方式下,Spring 可以先实例化 A 和 B,然后进行依赖注入,而不会出现死锁。
总结
- 构造器注入:当两个 Bean 之间形成循环依赖时,Spring 容器无法处理,因为容器需要先实例化对象后才能注入依赖,而构造器注入会要求对象完全创建后再注入,容易导致依赖无法满足。
- Setter 注入:Spring 可以先实例化 Bean,然后进行依赖注入,因此可以解决循环依赖问题。
- Spring 的三级缓存机制:通过保存正在创建中的 Bean,Spring 解决了循环依赖问题。
注意
- 循环依赖问题是单例 Bean 的问题,对于 原型 Bean,Spring 是不能解决循环依赖的,因为每次获取原型 Bean 时都会创建新的实例,Spring 不能进行类似的缓存机制。
10.观察者模式
观察者模式(Observer Pattern)是一种常见的设计模式,属于行为型模式。它定义了一种一对多的依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都能得到通知并自动更新。观察者模式适用于事件驱动的系统或用户界面系统中,能够解耦观察者和被观察者之间的关系。
基本概念
- 被观察者(Subject) :也叫“主题”,是状态发生变化的对象。它维护一个观察者的集合,提供注册、注销观察者的方法。当它的状态发生变化时,它会通知所有注册的观察者。
- 观察者(Observer) :是依赖于被观察者的对象,它会被通知到状态变化并做出相应的更新操作。
- 通知机制:当被观察者的状态发生改变时,观察者被通知,通常通过调用观察者的更新方法来实现。
UML类图
在 UML 类图中,观察者模式通常涉及三个主要角色:
- Subject:声明一个附加/删除观察者对象的接口。
- ConcreteSubject:实现 Subject 接口,维护状态并在发生变化时通知观察者。
- Observer:为所有具体的观察者定义一个更新接口。
- ConcreteObserver:实现 Observer 接口,接收通知并更新自身的状态。
观察者模式的流程
- 观察者注册到被观察者。
- 被观察者的状态发生变化。
- 被观察者通知所有已注册的观察者。
- 观察者收到通知后进行更新。
实例:天气预报
假设有一个天气预报系统,其中有一个 WeatherData 类,它充当被观察者,状态包括温度、湿度、气压等。当这些数据发生变化时,所有订阅该天气数据的观察者(如显示设备、警报系统等)都会被通知并更新自己的显示。
1. 定义观察者接口
javaCopy Code
public interface Observer {
void update(float temperature, float humidity, float pressure);
}
2. 定义被观察者接口
javaCopy Code
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
3. 具体的被观察者类
javaCopy Code
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
// 模拟气象数据的变化
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObservers();
}
}
4. 具体的观察者类
javaCopy Code
public class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity.");
}
}
5. 客户端代码
javaCopy Code
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
weatherData.registerObserver(currentDisplay);
weatherData.setMeasurements(80, 65, 30.4f); // 模拟气象数据变化
weatherData.setMeasurements(82, 70, 29.2f); // 模拟气象数据变化
}
}
输出结果
Copy Code
Current conditions: 80.0F degrees and 65.0% humidity.
Current conditions: 82.0F degrees and 70.0% humidity.
优缺点
优点:
- 松耦合:观察者模式解耦了被观察者和观察者之间的关系。观察者无需了解被观察者的具体实现,只需要关注状态变化即可。
- 扩展性强:添加新的观察者不需要修改现有的被观察者代码,符合开闭原则(对扩展开放,对修改关闭)。
- 动态更新:观察者可以动态订阅和取消订阅被观察者,可以根据需要更新。
缺点:
- 通知开销:如果观察者很多,通知所有观察者可能会带来性能问题。
- 依赖关系:观察者和被观察者之间有一定的依赖关系,虽然这种依赖是解耦的,但大量的观察者订阅可能会增加复杂性。
应用场景
- 事件处理系统:当事件发生时,需要通知多个监听器或处理器时(如图形用户界面中的按钮点击)。
- 实时数据更新:如股票市场、天气预报等系统,多个组件需要实时获取状态更新。
- 发布/订阅系统:消息系统、博客更新等,多个订阅者接收发布者发布的消息。
观察者模式使得系统的模块之间能够低耦合地交互,在动态变化的环境中尤为有用。
11.老年代年轻代
-
年轻代:用于存储新创建的对象,回收频繁且速度快,回收时使用复制算法,涉及 Eden 区和两个 Survivor 区。回收过程称为 Minor GC。
频繁回收:年轻代的回收频率较高,因为大多数对象的生命周期较短,很多对象会很快变为垃圾。
快速回收:年轻代的垃圾回收速度要求较快,以便尽量减少对应用的影响。 -
老年代:用于存储长时间存活的对象,回收较少且较慢,回收时使用标记-清除或标记-整理算法。回收过程称为 Major GC 或 Full GC。
较少回收:老年代的垃圾回收不应频繁发生,因为它通常涉及到更多对象,回收时需要的时间较长。
回收慢:老年代的回收过程通常比年轻代慢,因为它需要处理的对象比较多,并且清理老年代的时间较长。
年轻代与老年代的关系
- 晋升:当年轻代的对象经过多次垃圾回收后依然存活,它们会被晋升到老年代。这一过程通常通过“晋升阈值”来控制,阈值设定了多少次回收后对象才会晋升。
- 垃圾回收策略:年轻代的回收采用的是 复制算法,即将存活对象复制到另一区域,而老年代的回收一般采用 标记-清除 或 标记-整理 算法,这意味着它需要清理和整理更多存活的对象。
12.mysql 隔离级别
13.hashmap底层原理
HashMap 的底层原理详解
HashMap 是 Java 集合框架中最常用的数据结构之一,其核心设计目标是 快速存取键值对。它基于哈希表实现,结合了数组和链表(或红黑树)的优势。以下是其底层原理的详细解析:
一、数据结构
1. 底层存储
-
数组 + 链表/红黑树:
HashMap 内部维护一个Node<K,V>[] table数组,每个元素是一个链表或红黑树的头节点。-
链表:解决哈希冲突的简单方式,遍历链表查找目标节点(时间复杂度 O(n))。
-
红黑树:当链表长度超过阈值(默认 8)时,链表转换为红黑树,提升查询效率(时间复杂度 O(log n))。
-
转换条件:
- 链表长度 ≥ 8 且数组长度 ≥ 64 → 转红黑树。
- 红黑树节点数 ≤ 6 → 转回链表。
-
2. 节点结构
java
复制
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值(key.hashCode()的高位混合低位)
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点(链表或红黑树)
}
二、哈希函数
1. 计算哈希值
-
目标:将键的哈希码均匀分布到数组的索引位置,减少冲突。
-
步骤:
- 调用键的
hashCode()方法获取原始哈希码。 - 通过高位异或低位(扰动函数)进一步分散哈希值,减少碰撞概率。
- 与数组长度减一进行按位与运算,得到数组索引。
- 调用键的
java
复制
// 扰动函数(Java 8)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 计算索引
int index = (n - 1) & hash; // n 是数组长度(必须是 2 的幂)
2. 为什么数组长度是 2 的幂?
- 保证
(n - 1) & hash能均匀覆盖所有索引(0 到 n-1)。 - 例如,n=16 →
n-1=15(二进制1111),哈希值的低 4 位决定索引。
三、插入操作(put)
1. 插入流程
-
计算哈希值:通过
hash(key)得到哈希值。 -
定位桶:根据哈希值找到数组中的对应位置(桶)。
-
处理冲突:
-
桶为空:直接创建新节点。
-
桶不为空:
- 遍历链表或红黑树,若找到相同键(
key.equals(node.key)),更新值。 - 未找到相同键,插入链表尾部或红黑树中。
- 遍历链表或红黑树,若找到相同键(
-
-
扩容检查:插入后若元素数量超过阈值(
容量 × 负载因子),触发扩容。
2. 扩容机制
-
触发条件:元素数量 >
threshold(默认容量 × 0.75)。 -
扩容方式:
- 新数组大小为原数组的 2 倍。
- 重新计算所有元素的哈希值,迁移到新数组(rehash)。
-
优化:
- 通过
e.hash & oldCap判断节点在新数组中的位置(原索引或原索引 + 旧容量)。 - 减少 rehash 的计算量。
- 通过
四、查找操作(get)
-
计算哈希值,定位桶。
-
遍历链表或红黑树:
- 链表:逐个比较键(
equals())。 - 红黑树:通过二叉搜索树结构快速查找。
- 链表:逐个比较键(
-
返回值:找到匹配键则返回值,否则返回
null。
五、线程安全性
-
非线程安全:HashMap 不保证多线程环境下的数据一致性。
-
并发问题:
- 多线程同时扩容可能导致环形链表(死循环)。
- 数据覆盖或丢失(如两个线程同时插入不同键)。
-
解决方案:
- 使用
Collections.synchronizedMap包装成同步 Map。 - 使用
ConcurrentHashMap(分段锁或 CAS + synchronized)。
- 使用
六、性能影响因素
-
初始容量与负载因子:
- 初始容量过小 → 频繁扩容,性能下降。
- 负载因子过大 → 哈希冲突增多,链表/红黑树变长。
- 建议:根据预估数据量设置初始容量,负载因子默认 0.75(平衡空间与时间)。
-
哈希函数质量:
- 键的
hashCode()应尽量分散,减少冲突。 - 劣质哈希函数会导致性能退化为链表。
- 键的
七、Java 8 的优化
- 红黑树:链表过长时转换为红黑树,提升查询效率。
- 尾插法:解决头插法在多线程环境下的死循环问题。
- 扰动函数优化:通过高位异或低位减少哈希冲突。
八、与 ConcurrentHashMap 的区别
| 特性 | HashMap | ConcurrentHashMap |
|---|---|---|
| 线程安全 | 否 | 是(分段锁或 CAS + synchronized) |
| 锁粒度 | 无锁 | 锁桶(Java 8) |
| 扩容机制 | 单线程扩容 | 多线程协作扩容 |
| 适用场景 | 单线程或低并发 | 高并发环境 |
九、总结
- 核心原理:数组 + 链表/红黑树,通过哈希函数快速定位桶,处理冲突时动态调整结构。
- 优势:平均时间复杂度 O(1)(哈希均匀时)。
- 劣势:非线程安全,哈希冲突严重时性能下降。
- 适用场景:缓存、快速查询、非高并发环境。
理解 HashMap 的底层原理,有助于在开发中合理选择数据结构,避免性能瓶颈。
14.concurrenthashmap底层原理
ConcurrentHashMap 是 Java 中线程安全的哈希表实现,其核心设计目标是 高并发下的高性能。它在不同 Java 版本中经历了重大改进(尤其是 Java 7 到 Java 8),以下是其底层原理的详细解析:
一、核心设计思想
-
分段锁(Java 7)
- 将数据分成多个段(Segment),每个段独立加锁,支持多线程并发访问不同段。
- 缺点:内存开销大,段的数量固定(默认 16),扩容复杂度高。
-
CAS + Synchronized(Java 8+)
- 弃用分段锁,改用更细粒度的锁机制:对每个桶(Bucket)的头节点加锁(synchronized),结合 CAS 操作管理元数据。
- 优点:更高的并发度,减少锁竞争,支持动态扩容。
二、数据结构(Java 8+)
1. 底层存储
-
Node 数组:存储键值对的数组,每个元素是一个链表或红黑树的头节点。
java 复制 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; // ... } -
链表转红黑树:当链表长度超过阈值(默认 8)时转为红黑树,提升查询效率(时间复杂度从 O(n) 降到 O(log n))。
2. 桶数组(Table)
- 懒加载:首次插入数据时初始化数组,默认大小为 16。
- 动态扩容:当元素数量超过阈值(容量 × 负载因子)时,扩容为原数组的 2 倍。
三、并发控制机制
1. CAS 操作
-
用途:原子性地更新元数据(如桶数组指针
table、扩容标记sizeCtl)。 -
关键变量
sizeCtl:-1:表示初始化中。-2:表示扩容中。- 其他值:控制并发扩容的线程数(如
-(1 + n)表示有n个线程参与扩容)。
2. synchronized 锁
- 锁定粒度:仅锁定当前操作的桶(Bucket)的头节点,不影响其他桶。
- 锁升级:当链表转红黑树时,锁的粒度仍为头节点,避免全表锁。
四、扩容机制
1. 触发条件
- 初始化:首次插入数据时,若
table为空,触发初始化。 - 扩容:元素数量超过阈值(
sizeCtl控制),或迁移旧数组时发现新数组未完成。
2. 多线程并发迁移
- ForwardingNode:在扩容期间,旧桶的头节点会被替换为
ForwardingNode,新操作会被重定向到新数组。 - 数据迁移:多线程协作迁移旧数组中的节点到新数组,每个线程负责迁移固定数量的桶(如每次迁移 16 个桶)。
3. 渐进式扩容
- 双缓冲机制:新旧数组共存,逐步迁移数据,避免一次性阻塞所有操作。
五、读操作的无锁化
- volatile 变量:
Node的val和next字段均为volatile,保证可见性。 - 无需加锁:读操作直接访问
Node数组,依赖内存屏障保证数据一致性。
六、关键方法源码解析
1. put(K key, V value)
-
计算哈希值,定位桶位置。
-
若桶为空,通过 CAS 创建新节点。
-
若桶不为空,加锁处理:
- 链表:遍历插入或转红黑树。
- 红黑树:调用树节点的插入方法。
-
检查是否需要扩容。
2. get(Object key)
- 计算哈希值,定位桶位置。
- 遍历链表或红黑树,返回匹配的节点值。
- 无锁操作,依赖
volatile保证可见性。
七、Java 8 vs Java 7 的改进
| 特性 | Java 7(分段锁) | Java 8(CAS + synchronized) |
|---|---|---|
| 锁粒度 | 段级锁(默认 16 段) | 桶级锁(每个桶独立加锁) |
| 数据结构 | 数组 + 链表 | 数组 + 链表/红黑树 |
| 扩容机制 | 分段扩容 | 整体扩容,多线程协作迁移 |
| 并发性能 | 高吞吐,但内存开销大 | 更高并发度,低内存占用 |
| 锁类型 | ReentrantLock | synchronized(JVM 优化) |
八、适用场景
- 高并发读写:如缓存系统、计数器。
- 弱一致性要求:迭代器遍历时不阻塞其他操作(弱一致性)。
- 大数据量场景:支持动态扩容和高效内存管理。
九、性能调优建议
- 初始容量:根据预估数据量设置,避免频繁扩容(默认 16)。
- 负载因子:默认 0.75,平衡空间与时间效率。
- 并发控制:避免热点桶(如大量请求集中在同一哈希值)。
十、总结
ConcurrentHashMap 通过 CAS + synchronized 的细粒度锁 和 动态扩容机制,实现了高并发下的高性能。其核心优势在于:
- 读操作无锁:依赖
volatile和内存屏障。 - 写操作低竞争:仅锁定单个桶。
- 扩容高效:多线程协作迁移,渐进式完成。
理解其底层原理有助于在分布式系统、高并发场景中合理使用,避免性能瓶颈。
15.jvm调优
VM 调优的目标是 优化内存使用、减少垃圾回收(GC)停顿时间、提高系统吞吐量,同时避免内存溢出(OOM)和性能瓶颈。以下是调优的核心要点和步骤:
一、JVM 内存结构与关键参数
1. 内存区域划分
| 区域 | 作用 | 常见问题 |
|---|---|---|
| 堆(Heap) | 存放对象实例 | OOM、频繁 Full GC |
| 元空间(Metaspace) | 存放类元信息 | Metaspace OOM |
| 栈(Stack) | 方法调用和局部变量 | StackOverflowError |
| 本地方法栈 | Native 方法调用 | 内存泄漏 |
| 直接内存 | NIO 使用的堆外内存 | DirectBuffer OOM |
2. 核心调优参数
| 参数 | 作用 | 推荐值 |
|---|---|---|
-Xmx / -Xms | 最大/初始堆内存(建议设为相同值) | 物理内存的 1/4 ~ 1/2 |
-XX:NewRatio | 新生代与老年代比例(默认 2:1) | 2(年轻代占 1/3) |
-XX:SurvivorRatio | Eden 区与 Survivor 区比例(默认 8:1) | 8 |
-XX:MetaspaceSize | 初始元空间大小 | 256M(根据类数量调整) |
-XX:MaxMetaspaceSize | 最大元空间大小 | 512M ~ 1G |
-Xss | 每个线程栈大小 | 1M ~ 2M(高并发场景可减小) |
二、垃圾回收器选择
1. 常见 GC 算法对比
| GC 类型 | 适用场景 | 特点 |
|---|---|---|
| Serial GC | 单线程、小型应用 | 低开销,但 STW 时间长 |
| Parallel GC | 多核 CPU、高吞吐场景 | 多线程并行,适合批处理 |
| CMS(已废弃) | 低延迟应用(如 Web) | 并发标记清除,碎片化问题 |
| G1(推荐) | 大内存、低延迟场景 | 分区管理,可预测停顿时间 |
| ZGC/Shenandoah | 超大堆(TB 级)、极低延迟 | 亚毫秒级停顿,JDK 11+ 支持 |
2. 参数配置示例
-
G1 GC(默认 JDK 9+):
bash 复制 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 # 目标最大停顿时间 -XX:G1HeapRegionSize=32M # Region 大小(默认自动分配) -
ZGC(JDK 15+ 生产可用):
bash 复制 -XX:+UseZGC -Xmx16G # 支持最大 16TB 堆
三、调优步骤与工具
1. 调优步骤
-
监控与分析:
- 使用
jstat、jmap、jstack或 APM 工具(如 Prometheus + Grafana)收集性能数据。 - 分析 GC 日志(启用
-Xloggc:<file>和-XX:+PrintGCDetails)。
- 使用
-
定位瓶颈:
- 频繁 Full GC → 检查老年代内存是否不足或内存泄漏。
- 长暂停时间 → 优化 GC 算法或调整堆分区。
- CPU 过高 → 检查 GC 线程竞争或应用逻辑问题。
-
参数调整:
- 逐步调整参数,验证效果(如
-Xmx、-XX:MaxGCPauseMillis)。
- 逐步调整参数,验证效果(如
-
验证与迭代:
- 压力测试(如 JMeter)验证调优效果,持续监控。
2. 关键工具
| 工具 | 用途 |
|---|---|
jstat -gcutil | 实时监控 GC 状态 |
jmap -heap | 查看堆内存分布 |
jstack | 分析线程阻塞和死锁 |
| VisualVM | 图形化监控(CPU、内存、线程) |
| MAT(Memory Analyzer) | 分析堆转储文件,查找内存泄漏 |
四、常见问题与解决方案
1. OOM(内存溢出)
-
现象:
java.lang.OutOfMemoryError: Java heap space或Metaspace。 -
原因:
- 堆内存不足(对象过多或内存泄漏)。
- 元空间加载过多类(动态生成类,如反射、CGLIB)。
-
解决:
- 增大
-Xmx或-XX:MaxMetaspaceSize。 - 使用 MAT 分析堆转储,排查内存泄漏。
- 增大
2. 频繁 Full GC
-
现象:GC 日志中频繁出现
Full GC (System.gc())。 -
原因:
- 老年代空间不足(对象过早晋升)。
- 显式调用
System.gc()(可通过-XX:+DisableExplicitGC禁用)。
-
解决:
- 增大老年代大小(调整
-XX:NewRatio)。 - 优化对象生命周期,减少短命大对象。
- 增大老年代大小(调整
3. CPU 过高
-
现象:GC 线程占用大量 CPU。
-
原因:
- GC 频繁触发(如堆内存过小)。
- 应用代码存在死循环或计算密集型操作。
-
解决:
- 优化 GC 参数(如增大
-Xmx或调整 GC 算法)。 - 使用性能分析工具(如 async-profiler)定位代码热点。
- 优化 GC 参数(如增大
五、高级调优技巧
-
逃逸分析:
- 启用
-XX:+DoEscapeAnalysis,让 JVM 自动优化栈上分配对象,减少堆压力。
- 启用
-
大对象直通老年代:
- 调整
-XX:PretenureSizeThreshold(默认 0),避免小对象过早晋升。
- 调整
-
GC 日志分析:
- 关注
Young GC和Full GC频率、耗时及内存回收效率。
- 关注
六、调优总结
| 目标 | 策略 |
|---|---|
| 降低延迟 | 选择 G1/ZGC,调整 -XX:MaxGCPauseMillis |
| 提高吞吐量 | 使用 Parallel GC,增大堆内存 |
| 避免 OOM | 监控内存使用,优化代码和配置 |
关键原则:
- 调优需结合应用场景(如高并发、大数据量)。
- 避免过度调优,优先通过监控定位瓶颈。
- 生产环境建议使用 G1/ZGC 等现代 GC 算法。
16.redis过期策略
. 定时删除(TTL-based Deletion)
- 原理:
在设置键的过期时间时,创建一个定时器(Timer),键到期时立即删除。 - 优点:
内存精准释放,过期键立即清理,无内存浪费。 - 缺点:
CPU 资源消耗高,大量键同时过期时,定时器触发可能引发性能抖动。 - 适用场景:
对内存敏感但对延迟不敏感的场景(如缓存非关键数据)。
2. 惰性删除(Lazy Deletion)
- 原理:
当客户端访问某个键时,Redis 检查其是否过期,若过期则删除。 - 优点:
节省 CPU 资源,仅在访问时触发检查,避免无谓的删除操作。 - 缺点:
内存泄漏风险,未访问的过期键长期占用内存。 - 适用场景:
读多写少且过期键访问频率低的场景(如日志缓存)。
3. 定期删除(Periodic Deletion)
-
原理:
周期性随机抽查一定数量的键,删除其中已过期的键。 -
优点:
平衡 CPU 与内存,避免定时删除的 CPU 峰值和惰性删除的内存泄漏。 -
缺点:
需合理配置抽查频率和数量,否则可能清理不彻底。 -
适用场景:
通用场景,推荐生产环境默认选择。
Redis 的默认策略组合
Redis 默认采用 定期删除 + 惰性删除 的组合策略:
-
定期删除:
- 默认每秒执行
10次(由hz配置项控制)。 - 每次随机检查
20个键,删除其中过期的键。 - 若过期键比例超过
25%,则继续扫描下一批。
- 默认每秒执行
-
惰性删除:
- 在
GET、EXISTS等命令执行时,检查键是否过期并删除。
- 在
内存淘汰策略(maxmemory-policy)
当内存不足时,Redis 会根据策略主动删除键(与过期策略互补):
| 策略 | 描述 |
|---|---|
volatile-lru | 从设置过期时间的键中,按 LRU 淘汰 |
allkeys-lru | 从所有键中按 LRU 淘汰 |
volatile-random | 从设置过期时间的键中随机淘汰 |
allkeys-random | 从所有键中随机淘汰 |
volatile-ttl | 从设置过期时间的键中,按剩余时间淘汰 |
noeviction | 不删除,返回错误(默认) |
17.hashcode和equals
hashCode 是 Java 中 Object 类的一个方法,用于返回对象的哈希码。哈希码是一个整数值,它用于支持哈希表等数据结构的高效存储和检索操作。哈希码通常用于在集合(如 HashMap、HashSet 等)中快速查找对象。
具体含义:
hashCode方法为每个对象生成一个整数值(哈希码),这个值通常基于对象的内存地址或对象的内容计算出来。- 如果两个对象通过
equals()方法被认为是相等的,它们的hashCode()也应该相等。 - 然而,两个不同的对象的
hashCode()不一定要不同,这意味着不同的对象可能会有相同的哈希码(这种情况叫做哈希冲突)。
为什么需要 hashCode:
hashCode 的主要作用是在哈希表中快速定位对象。例如,在 HashMap 中,当我们使用一个键来查找一个值时,首先会计算键的 hashCode,然后将其映射到一个桶(bucket)中,最终进行比较以找到对应的值。这大大提高了查找效率。
hashCode 和 equals 是两个密切相关的方法,通常一起使用来决定对象的相等性
hashCode用于返回对象的哈希值,主要用于哈希集合中查找和存储对象。equals用于判断两个对象的内容是否相同,通常用于对象比较。- 在自定义类中,如果重写了
equals,通常也需要重写hashCode,以保证哈希集合的正确行为。
== 是 Java 中的比较操作符,用于比较两个变量或对象的内存地址或数值。
equals() 是 Object 类中的方法,默认情况下也用于比较两个对象的引用地址,即判断两个对象是否为同一个对象。然而,许多类(如 String、Integer)都会重写 equals() 方法,使其能够比较对象的内容。
- 如果两个对象根据
equals方法比较是相等的(即obj1.equals(obj2)返回true),那么它们的hashCode方法必须返回相同的整数值。 - 但是,如果两个对象的
hashCode相同,并不一定意味着它们是相等的。这就需要进一步通过equals方法来验证。
为什么需要同时重写 hashCode 和 equals?
- 哈希表的高效性:哈希表(如
HashMap、HashSet)是通过对象的hashCode来进行数据存储和查找的。如果你没有正确重写这两个方法,可能会导致哈希表在处理相同对象时出现错误,影响查找和删除操作的正确性。 - 防止哈希冲突问题:通过自定义
hashCode和equals,可以避免哈希冲突的影响,确保哈希表操作的正确性。
// true
String a ="1";
String b ="1";
System.out.println(a==b);
System.out.println(a.equals(b));
System.out.println(a.hashCode()+"--"+b.hashCode());
// false
JSONObject a =new JSONObject();
JSONObject b =new JSONObject();
System.out.println(a==b);
System.out.println(a.equals(b));
System.out.println(a.hashCode()+"--"+b.hashCode());
哈希算法是将任意长度的输入数据映射为固定长度的输出值的函数,广泛用于计算机科学中的数据存储、数据校验、安全通信等多个领域。常见的哈希算法包括 MD5、SHA-1、SHA-256 等,其中 SHA-256 被认为在大多数应用中具有较好的安全性。
哈希冲突是不可避免的,因为哈希函数将无限大小的输入映射到有限的输出空间。对于关键应用,选择抗碰撞性强的哈希算法非常重要,如 SHA-256 或 SHA-3。同时,现代加密技术也可以通过“加盐”或“增强的加密协议”来降低哈希冲突对系统安全的威胁。
18.hashmap扩容机制
HashMap 在 Java 中是一个常用的键值对映射集合,它的扩容机制对于性能的优化非常重要。HashMap 的扩容是动态调整内部存储数组(即桶数组)的大小,以确保其在插入大量数据时依然能保持良好的查找性能。
1. HashMap 的内部结构
HashMap 使用一个 数组 和链表(或红黑树,在高负载时)来存储键值对数据。每个键值对通过哈希值被映射到数组的一个索引位置,如果不同的键有相同的哈希值,它们会被存储在同一个索引处,形成一个 链表(或红黑树)。当链表的长度超过一定阈值时,会将链表转化为红黑树来提高查找效率。
2. HashMap 扩容的触发条件
HashMap 默认的初始容量为 16,负载因子为 0.75。当插入的元素数量超过当前容量和负载因子的乘积时,HashMap 会触发扩容。扩容时,HashMap 会将容量翻倍,并重新计算所有键的哈希值并映射到新的数组中。具体的触发条件如下:
- 负载因子:负载因子是一个衡量
HashMap在扩容前填充的程度的参数,默认值为 0.75。它表示当HashMap中的元素数量达到capacity * loadFactor时,触发扩容。 - 容量:
HashMap的初始容量是 16,容量在扩容时会翻倍。即从 16 扩展到 32,再到 64,以此类推。
假设当前 HashMap 的容量为 n,负载因子为 0.75,当元素个数大于 n * 0.75 时,就会触发扩容。扩容后,新的容量为原来的两倍,并且元素会根据新的容量重新分配。
3. 扩容的具体流程
扩容是 HashMap 的一个相对昂贵的操作,因为它需要执行以下步骤:
- 分配新的数组:将原来数组的大小翻倍,创建一个新的数组,新的数组的大小是原数组的两倍。
- 重新哈希:将原来数组中的所有元素(键值对)重新计算哈希值,并根据新的数组容量,将元素重新分配到新的数组位置。由于数组容量的变化,哈希值映射的位置可能发生改变。
- 迁移元素:将所有元素从旧的数组迁移到新的数组中,这个过程需要遍历所有原数组中的元素,重新计算哈希并插入到新数组中。
4. 扩容对性能的影响
扩容通常是一个耗时的操作,因为它涉及到创建新数组并重新分配所有元素。扩容时,HashMap 的时间复杂度会变成 O(n) ,其中 n 是当前 HashMap 中元素的个数。因此,扩容操作会对性能产生一定影响,特别是在元素数量大时。
为了避免频繁的扩容,可以通过合理设置 HashMap 的初始容量和负载因子来减少扩容次数。例如,如果你已知 HashMap 将要存储的元素数量较大,可以通过 new HashMap(initialCapacity, loadFactor) 来初始化一个合适大小的 HashMap。
5. 负载因子的作用
负载因子决定了 HashMap 何时进行扩容。较小的负载因子意味着 HashMap 会更早进行扩容,而较大的负载因子则会推迟扩容的发生。负载因子的默认值是 0.75,这通常在性能和内存消耗之间提供了一个平衡。
- 较低的负载因子(例如 0.5)意味着
HashMap会较早进行扩容,内存使用较为宽松,可能会提高查询性能。 - 较高的负载因子(例如 0.9)会推迟扩容,减少内存消耗,但可能会导致查询性能下降,因为更多元素集中在同一个桶内,增加了碰撞的概率。
6. 扩容后的影响
扩容会导致 HashMap 的哈希冲突减少,因为桶的数量翻倍。扩容后,新的数组桶数量更大,哈希冲突的概率较小,从而可以提高查询和插入的效率。
7. 总结
HashMap扩容的触发条件是元素数量达到当前容量与负载因子乘积的阈值。- 扩容时,容量会翻倍,并且所有元素会重新计算哈希值并重新分配到新的数组中。
- 扩容会影响性能,尤其在大量数据插入时,可能会导致性能瓶颈。
- 通过合理设置初始容量和负载因子,可以减少不必要的扩容操作,优化性能。