文章目录
- 前沿
- [redis 主要学习的内容](#redis 主要学习的内容)
- reactor模式
-
- 什么是reactor模式
-
- [Reactor 模式的三大核心组件](#Reactor 模式的三大核心组件)
- reactor模式的应用实例
- reactor模式在redis中的应用
- 初始化
- [redis 6.0+ 的多线程](#redis 6.0+ 的多线程)
- redis的数据类型
- 其他
前沿
本代码学习并不是基于redis最新版本代码的学习,而是基于6.2进行学习的,原因如下
- 基于大模型进行辅助学习,大模型的回答大多数不是基于最新版本的回答(主要原因)
- 大部分企业用的redis版本也不是最新的版本,如果不是基于新版本的功能开发没必要学习最新版本
- 最新版本做了一些抽象,更加标准化和模块化,不如函数调用看起来更直接,学习起来简单
redis 主要学习的内容
包含的技术点
- reactor模式
- 零拷贝
- IO多路复用
- 渐进式哈希
- 高效的数据结构(SDS)/内存优化
- LRU
支持的类型
- 字符串
- 哈希
- list
- set
- zset
持久化
- AOF
- RDB
- 混合持久化(AOF+RDB)
生产环境禁止使用save命令,因为他会阻塞主线程的执行,推荐使用bgsave,使用后台线程进行持久化
reactor模式
什么是reactor模式
Reactor 模式是一种处理高并发I/O事件的模式,其核心思想是**用一个线程(或少量线程)监听所有事件,当某个事件就绪时,就分发给对应的处理函数(Handler)来处理**
Reactor 模式的三大核心组件
- Reactor(反应器/事件分发者)
职责: 整个模式的大脑。它运行在一个独立的线程中,负责监听和分发事件。
工作: 它通过一个"多路复用器"等待事件发生。当有事件(如连接建立、数据可读、数据可写)发生时,它会将这个事件分发给对应的处理程序。 - Demultiplexer(多路复用器/事件分离器)
职责: Reactor用来监听事件的工具。在Linux上通常是 epoll、kqueue(BSD/Mac)或 IOCP(Windows)。
工作: 它可以同时监视成千上万个文件描述符(如Socket连接),并告诉Reactor哪些描述符已经就绪(有数据可读、可以写入等),而不用为每个描述符创建一个线程去轮询。这是高并发的技术基础。 - EventHandler(事件处理器)
职责: 实际处理业务逻辑的组件。它是一个接口或抽象类,定义了处理各种事件的方法(如 handle_read, handle_write)。
工作: 每个网络连接通常会对应一个EventHandler实例。当Reactor将事件分发给它时,它的对应方法会被回调,执行实际的读取、解码、计算、编码、发送等操作。
reactor模式的应用实例
Netty:Java领域最著名的高性能网络框架,基于主从Reactor模式。
Nginx:高性能的HTTP/反向代理服务器,使用Reactor模式。
Redis:单Reactor单线程模型的代表。
Node.js:其事件循环机制就是Reactor模式的典型实现。
reactor模式在redis中的应用
Redis是Reactor模式应用的典范,它采用了单Reactor单线程的架构。
redis 主要处理两种事件
- 文件事件(File Events) - 处理网络I/O
c
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc; // 读事件处理回调函数
aeFileProc *wfileProc; // 写事件处理回调函数
void *clientData; // 客户端带来的数据,常用的比如命令行及参数,连接/认证信息
} aeFileEvent;
- 时间事件(Time Events) - 处理定时任务
时间事件的结构体没有什么说的就省略了,时间事件主要处理2类
- 内存淘汰(evictionTimeProc)
- 周期性的服务器维护任务(serverCron)
初始化
信号初始化
就像开车一样,上来应该先学刹车。首先需要学习的第一个知识点是优雅退出
c
void setupSignalHandlers(void) {
struct sigaction act;
// 清空要阻塞的信号集?我们应该这么理解行代码的作用
// 如果不执行 sigemptyset(&act.sa_mask),act.sa_mask 的初始值是随机的(内存垃圾值),可能导致信号处理期间意外阻塞某些关键信号
// 这是 Linux 信号处理的标准操作,如果没有这一步可能引起处理信号混乱,甚至出现信号不能被处理的情况
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 注册一个优雅退出的回调函数,这个函数是有用户自己定义,由操作系统在服务程序退出的时候调用
act.sa_handler = sigShutdownHandler;
// 下面是向操作系统注册两个信号(2和15)的处理函数
sigaction(SIGTERM, &act, NULL);
sigaction(SIGINT, &act, NULL);
// redis 没有处理SIGHUP信号,而是选择了SIG_IGN。它是通过redis-cli set 来进行不重启修改配置的,因为重启会丢掉redis的内存的配置数据
setupDebugSigHandlers();
}
在go里面一般是通过os.Notify这么处理
go
func pocessSig()
// 设置信号处理
quit := make(chan os.Signal, 1)
// 监听系统信号
signal.Notify(quit,
syscall.SIGINT, // Ctrl+C
syscall.SIGTERM, // kill
syscall.SIGQUIT, // Ctrl+\
syscall.SIGHUP, // 终端断开
)
// 启动 goroutine 监听信号(不阻塞主流程)
go func() {
// 阻塞等待信号
sig := <-quit
// 通过 switch 匹配信号类型,执行对应处理函数
switch sig {
case syscall.SIGINT:
handleSIGINT()
case syscall.SIGTERM:
handleSIGTERM()
case syscall.SIGQUIT:
handleSIGQUIT()
case syscall.SIGHUP:
handleSIGHUP()
default:
fmt.Printf("接收到未知信号:%v\n", sig)
}
// 所有信号处理完成后,退出程序(可根据需求调整)
fmt.Println("\n所有清理工作完成,程序退出")
os.Exit(0)
}()
// 其他正常逻辑
}
// 定义不同信号的处理函数
// 处理 SIGINT (Ctrl+C)
func handleSIGINT() {
fmt.Println("\n=== 接收到 SIGINT (Ctrl+C) 信号 ===")
fmt.Println("执行 SIGINT 专属逻辑:优雅停止用户交互...")
// 这里可以添加实际业务逻辑,比如关闭用户输入、保存临时数据等
time.Sleep(1 * time.Second) // 模拟处理耗时
}
// 处理 SIGTERM (kill 命令)
func handleSIGTERM() {
fmt.Println("\n=== 接收到 SIGTERM (kill) 信号 ===")
fmt.Println("执行 SIGTERM 专属逻辑:释放资源、持久化数据...")
// 实际场景:关闭数据库连接、写入日志、保存缓存等
time.Sleep(1 * time.Second)
}
// 处理 SIGQUIT (Ctrl+\)
func handleSIGQUIT() {
fmt.Println("\n=== 接收到 SIGQUIT (Ctrl+\\) 信号 ===")
fmt.Println("执行 SIGQUIT 专属逻辑:生成核心转储、打印堆栈信息...")
// 实际场景:输出 goroutine 堆栈、记录程序状态等
time.Sleep(1 * time.Second)
}
// 处理 SIGHUP (终端断开)
func handleSIGHUP() {
fmt.Println("\n=== 接收到 SIGHUP 信号 ===")
fmt.Println("执行 SIGHUP 专属逻辑:重新加载配置文件...")
// 实际场景:重新读取配置、重启子进程等
time.Sleep(1 * time.Second)
}
创建共享对象
redis创建共享对象是为了优化内存的使用。通过共享小整数(共享整数范围:0-9999)和常用字符串减少重复内存的使用
共享的内容
- 小整数:0-9999(这1w个数字只存储一次,其他的都是引用,只需要一个指针就好)
- 常见响应字符串:"OK"、"PONG"、"QUEUED"等
- 错误消息:"ERR"、"WRONGTYPE"等
- 特殊值:"nil"、空数组等
优点:
- 节省内存
- CPU缓存友好;共享对象常驻内存,缓存命中率高
- 减少内存回收压力(共享对象永不释放)
- 计算速度快。不需要比较值,只需要比较地址即可
缺点: - 初始化时启动时间稍慢
- 固定集合,不能动态添加共享对象
- 内存常驻(关键在于不要设计过多使用频率少的兑现),即便不使用也占用内存
event初始化
在redis中一共分为5大类事件类型
- 文件事件(重点):主要是处理网络IO的多路复用
- 时间事件:定时任务 主要对应ServerCron函数处理周期性任务
- 键值的过期处理(需要结合清理策略)
- 自身状态的管理(比如统计动态更新手动set的配置
- 内存管理和资源回收
- 持久化相关的调度
- 集群场景下心跳发送与状态检查
- 模块事件:模块扩展,一般用于添加自己开发命令行或者调整缓存调整策略这种高定制化的开发
- 子进程事件:子进程监控(主要是bgsave和AOF重写的时候需要创建子进程,其他情况很少见)避免僵尸进程
- 信号事件:信号处理
redis 6.0+ 的多线程
6.0之前所有的处理都在main线程里完成,从6.0版本开始支持多线程处理网络请求中的读取和解析命令行的处理,只是处理数命令行、协议解析和把主线程执行完的结果写回socket实际的get/set还是由main线程来处理。redis的默认配置中多线程其实没有开启,默认还是单线程工作。举个简单的例子就是,io-thread就是一个CEO助理,只管写PPT(对标协议解析),实际上场讲PPT的人还是我们至高无上的CEO!CEO讲完后面的执行还是助理干(对标sendResponse),一般10w qps一下或者没有大key的情况先不需要开,就算设置这个io-thread 线程数也不推荐太多,3-6个基本上够了
这个值在代码里的默认值是1,配置文件内默认值是4(被注释掉了)
有人可能和我一样,觉得redis不是已经使用了系统的零拷贝技术,为什么还需要io-thread来搬数据?
其实他们两个解决的是不同层面的问题。io-thread其实是解决的协议解析和格式化的的工作,这块应该算是CPU密集型的工作。零拷贝专注的是数据传输路径优化,这两个并不冲突。应用层使用io多线程,系统层面使用的是零拷贝技术
redis的数据类型
string
redis 的string不是直接使用C语言的string,而是自己实现了redis的SDS结构进行高效的管理内存
使用SDS的好处(对比C语言)
- 二进制安全(C语言存储中间包含\0 的字符串会被截断)
- 预分配空间策略
- 获取字符串长度为O(1)
- 协议就是依赖字符串长度进行操作的
- 输入缓冲区管理(缓冲区扩容)
- 编码格式的选择根据字符串长度决定
- 持久化也需要计算长度
- 存储数据的时候数据的类型自动升级
SDS对于内存分配的优化
- 如果字符串长度小于等于44个字节就只分配一次内存(为什么是44个字节,其实是现代CPU的行缓存是64字节,头部总计<robj 16bytes, sdshrd 3bytes,\0 1byte>占20个字节),数据直接写在header后面
- 如果大于44个字节需要申请两次内存,sds+实际数据长度打包申请一次,然后再创建申请robj对象,最后把sds放在robj->ptr中。
- 如果执行
set name 123和set name '123'或者set name "123"结果是一样的,都会以int类型进行存储,这是由redis的协议RESP决定的
redis中string类型的O(1)查询的实现
通过字典(dict)实现的,底层使用哈希表结构。
解决哈希冲突的方法是链地址法
- 每个哈希槽维护一个单向链表
- 发生冲突时,新元素插入到链表头部
- 查询时遍历链表,使用memcmp比较键值
当负载因子过高或者过低时都会使用渐进式哈希进行平衡,避免一次性迁移导致服务阻塞:
- 准备阶段:创建新的哈希表ht[1],大小为ht[0]的2倍或1/2
- 迁移阶段:每次操作时,将ht[0]中的部分键值对迁移到ht[1]
为什么读取时是先迁移再读取?
- get的时候先判断是不是在迁移阶段
- 如果是迁移阶段,需要先执行一个迁移操作,把旧哈希表桶里的元素迁移到新哈希表
然后再从h[0]这个表里查找,如果查不到就去h[1]表里查
这样是为了方式get并发太大的时候CPU让不出来,没办法继续rehash,让rehash阶段过长。
c
// 6.2版本的源码
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 0) return NULL; /* dict is empty */
// rehash 阶段先迁移一个桶,然后再查
// 这样是为了方式get并发太大的时候CPU让不出来,没办法继续rehash
// 导致rehash阶段过长。
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
这里有个点需要注意一下,如果是set name 1230 如果是key已经存在直接修改key的值,如果是新添加这时候需要看是不是在rehash节点,如果是就插入到h[1],否则插入到h[0]
c
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing){
...
// 不管是不是存在都要先迁移一个桶
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
// 如果key存在就直接返回了
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently. */
// 正在迁移就新增到h[1],否则迁移到h[0]
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
...
}
特殊情况处理:
BGSAVE/BGREWRITEAOF执行时:即使负载因子>5也不会触发扩容,避免内存占用翻倍影响持久化性能
list
首先先澄清一个关于压缩链表的误区:
压缩链表不是有什么更厉害的技术可以压缩数据或者使用zip等压缩算法进行压缩数据,而是设计一个更紧凑的数据结构
使用场景:
- 消息队列
- 栈
redis 3.2版本之前的实现是双向链表和ziplist配合实现的,小数据量和元素不多的时候使用ziplist进行存储,大数据量或者元素比较多的时候自动退化成双向链表进行存储
tex
这里说下退化成链表的存在的一些问题:
- 数据结构的转变会浪费一些性能,因为需要调用系统调用申请更多次的内存
- 双向链表有前驱指针和后继指针和data的指针,这3个指针至少需要24字节的空间,如果你list元素本身比较小,大部分内存都浪费在维护链表上了
redis 3.2版本之后对上面的实现进行了整合,把ziplist 包装在了双向链表里面,继承了ziplist紧凑列表同时也继承了双向链表的灵活性!
简单说下为什么quicklist为什么能节省空间
ziplist 是一块连续的内存空间,他的访问是通过prevlen来进行标记每个list元素的长度,读取的时候通过prevlen来确定从哪开始读取一个元素,读取几个字节这种方式去读取元素。如果你是初步学习源码,你暂时可以把ziplist理解成数组(只不过他不是一个普通数组,是可以存放不同类型元素的数组)这样他就不需要维护链表那样前驱指针和后继指针了,更多的空间来存储用户数据。
这个设计能大幅度提高内存的利用率,但是他也不是完美的,原因如下:
- 每次添加元素都要重新申请一段新的内存(每个元素长度不固定,预分配内存可能不能解决问题),先把原来的数据拷贝过来,然后再把新数据存进去(所以推荐使用list的时候批量push)
- 如果是Lpush的时候可能会引起移动所有的数据,并且修改prevlen,因为当字符串大于255的时候编码方式有变化,encoding字段需要占用5个字节的空间,时间复杂度可能是O(N*N)
- 删除指定元素的可能时间复杂度是O(N*N)

quickNodeList 什么时候添加新节点
在redis.conf里面有个配置list-max-ziplist-size -2 每个ziplist 最大申请8kb的空间什么时候存满数据什么时候新增一个新的双向链表节点
set
主要使用场景(不需要顺序的范围存储及查询)
- 黑白名单(ip、拉黑/关注朋友)
- 需要求交并集操作(好友推荐、感兴趣的广告/文章推送)
- 抽奖、随机抽样等
- 去重
zset
使用场景(有序的范围查找或者正排倒排等):
- 排行榜(积分/跑步)
- 时间序列
zset 在redis中是通过跳表和哈希表进行实现的。
c
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
哈希表
先简单说下为什么使用哈希表?
肯定是为了快啊,用在什么地方呢?
- 快速成员查找(ZSCORE 命令)
- 快速分数更新
- 成员存在性判断
虽然哈希表增加了额外的内存开销(每个成员需要存储额外的指针和分数),但 Redis 通过压缩列表优化(小集合使用压缩列表)和智能编码(整数编码优化)来平衡内存使用。当集合元素较少时,使用压缩列表节省内存;当元素较多时,才转换为跳表+哈希表的组合结构。
c
// server.zset_max_ziplist_value 和 server.zset_max_ziplist_entries == 0 这两个条件决定的
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
如果ziplist没有关闭并且超过了zip阈值会把ziplist转化成跳表的实现
c
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
...
// 下面就是转换成跳表的源代码
/* Note that the above block handling ziplist would have either returned or
* converted the key to skiplist. */
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
de = dictFind(zs->dict,ele);
if (de != NULL) {
/* NX? Return, same element already exists. */
if (nx) {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
curscore = *(double*)dictGetVal(de);
/* Prepare the score for the increment if needed. */
if (incr) {
score += curscore;
if (isnan(score)) {
*out_flags |= ZADD_OUT_NAN;
return 0;
}
}
/* GT/LT? Only update if score is greater/less than current. */
if ((lt && score >= curscore) || (gt && score <= curscore)) {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
if (newscore) *newscore = score;
/* Remove and re-insert when score changes. */
if (score != curscore) {
znode = zslUpdateScore(zs->zsl,curscore,ele,score);
/* Note that we did not removed the original element from
* the hash table representing the sorted set, so we just
* update the score. */
dictGetVal(de) = &znode->score; /* Update score ptr. */
*out_flags |= ZADD_OUT_UPDATED;
}
return 1;
} else if (!xx) {
ele = sdsdup(ele);
znode = zslInsert(zs->zsl,score,ele);
serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
*out_flags |= ZADD_OUT_ADDED;
if (newscore) *newscore = score;
return 1;
} else {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
} else {
serverPanic("Unknown sorted set encoding");
}
return 0; /* Never reached. */
}
什么是跳表
没有一步路是白走的这句话真实的刻画了跳表的查找过程
跳表是一种基于链表的有序数据结构,通过给链表添加多层索引实现快速查找和删除,平均时间复杂度是O(log N)。
tex
需要注意一点:
这里说的链表是单项链表和双向链表的结合
L0层的链表是双向链表,其他层的都是单项链表(换句话说就是:除了L0层每个节点在每一层都有一个 forward指针,只能单向向后移动)
高层 (L2): H → 30 → 70 → 100
↓ ↓ ↓ ↓
中层 (L1): H → 10 → 30 → 50 → 70 → 90 → 100
↓ ↓ ↓ ↓ ↓ ↓ ↓
底层 (L0): H ⇄ 10 ⇄ 20 ⇄ 30 ⇄ 40 ⇄ 50 ⇄ 60 ⇄ 70 ⇄ 80 ⇄ 90 ⇄ 100
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
双向链表(backward指针)
结构体
c
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;// 元素数据的的地址指针
double score; // 排序的依据
struct zskiplistNode *backward; // 后继指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前驱节点,只有在L0层中才有意义
unsigned long span; // 元素的跨度
} level[]; // 注意:这里没有指定数组大小!
} zskiplistNode;
跳表的索引
跳表的索引是一个柔性数据,每个元素在他所属的层级的位置以及它下层中的指针都在柔性数组中存储,下降到下一层的时候继续走就行了,下降到下一层的时候不需要从链表头开始查找
举例
tex
L3: H → 100
L2: H → 30 → 70 → 90 → 100
L1: H → 10 → 30 → 50 → 70 → 80 → 90 → 100
L0: H → 10 → 20 → 30 → 40 → 50 → 60 → 70 → 80 → 90 → 100
节点 70 的内存布局:
┌─────────────────────────────────────────┐
│ 1. 数据部分: │
│ - ele: 指向存储"70"的SDS字符串 │
│ - score: 70.0 (double类型) │
│ - backward: 指向节点60的指针 │
├─────────────────────────────────────────┤
│ 2. L0层索引 (level[0]): │
│ - forward: 指向节点80 │
│ - span: 1 (从70到80跳过1个节点) │
├─────────────────────────────────────────┤
│ 3. L1层索引 (level[1]): │
│ - forward: 指向节点80 │
│ - span: 1 (从70到80跳过1个节点) │
├─────────────────────────────────────────┤
│ 4. L2层索引 (level[2]): ← 重点关注! │
│ - forward: 指向节点90 │
│ - span: 2 (从70到90跳过2个节点) │
└─────────────────────────────────────────┘
比如你查score=80的节点,只要比你目标值score值大的就下降一层,比你目标值小的就往前移动一次,直到找到目标值,如果到L0也没找到,那就是值不存在
为什么使用跳表,如果不使用跳表还有哪些方案能实现
红黑树我们就不说了,每次查询都得遍历,还得保持平衡。现在我们给B+输做个对比
- 数据都在B+树的叶子节点上,非叶子节点是索引。叶子节点上的数据也是双向链表,支持范围查找。
但是在实现上B+树更复杂,数据库存储引擎使用B+树是因为B+树对磁盘IO友好,redis是一个内存数据库,对磁盘不敏感。 - zset 使用跳表维护有序性,使用哈希表快速的查找元素
- 排除IO磁盘优势以后性能相差不大,跳表的实现比B+树更简单
hash
其实哈希的使用场景很有限,很多资料上都说哈希适合存结构体,不过实际工程中几乎没有人使用哈希来存结构体,尽管他支持局部字段修改,但是他不支持修改嵌套以后的结构体 ,这就很难受了。
使用场景:
- 用户信息(不太频繁改动也不怎么复杂)
- 购物车
其他
问java程序员一个问题
java内库里本身就有queue和stack为什么还用redis来实现栈或者队列?
tex
其实原因很简单,java内库里面的栈或者队列只能供你自己这个程序使用,如果有多个程序需要共享数据怎么办?是不是用redis这个中间件很适合不需要你自己维护数据同步和持久化的问题?
补充引用技术和gc技术
GC的算法
- 引用计数
多一次引用,计数器+1,释放的时候计数器-1,减到0立即释放。垃圾回收分散到每个引用操作上,不存在STW程序停摆的情况
让内存得到更高效的应用而且实现简单
但是引用计数有个致命的缺点,解决不了循环引用的问题(相互引用的对象无法自动回收)会导致内存泄露(这块可以让分代GC来弥补,python就是这么实现的,周期性的清理循环引用)
维护计数器需要额外的内存空间,如果高并发场景下可能会有锁竞争
python
# 创建循环引用
node1 = Node(1) # 引用计数=1
node2 = Node(2) # 引用计数=1
node1.child = node2 # node2引用计数=2
node2.parent = node1 # node1引用计数=2
代表语言:python(其实python是引用计数+分代gc)
-
三色标记
三色标记法是一种
增量式、并发式的垃圾回收算法,用于解决传统标记-清除算法的 STW(Stop-The-World) 问题。解释名词:
- 增量式:增量标记是将标记过程拆分成多个小步骤,在应用程序执行过程中穿插进行,而不是一次性完成整个标记过程。这样做的目的是减少STW(Stop-The-World)停顿时间,让应用程序能够更流畅地运行
- 并发式: 这里说的并发执行是说和服务进程并发执行,垃圾回收期间不影响服务程序的正常执行
tex
这里与应用程序并发执行减少了STW,但是也有一个致命的缺点:一但开始标记,新申请的内存怎么着色;
这里就引入一个新的技术叫 写屏障
当应用程序在标记过程中创建新对象时,写屏障会拦截这个操作,并立即将新对象标记为黑色。这样做的原因是为了维护三色不变式,确保垃圾回收的正确性。
如果新对象引用其他对象是白色,为了保证它不会被回收也会把他标记成灰色并放入垃圾回收器的待扫描队列,如果不是白色不用处理
三色标记法的原理是它将对象标记为三种颜色
| 颜色 | 状态 | 含义 |
|---|---|---|
| 白色 | 未扫描到 | 可能是潜在垃圾或者还没有被扫描到 |
| 灰色 | 已访问,子对象未访问 | 中间状态,需要继续扫描 |
| 黑色 | 已访问,子对象已扫描完成,或者对象已经没有子对象了 | 活跃对象,不能回收 |
gc从根对象(全局变量、栈变量、寄存器)开始标记
代表语言:go,java(G1)
-
分代
基于对象生命周期特征的优化策略,其核心思想是"弱分代假说":大多数对象都是朝生夕死的,存活时间很短。通常分为:
年轻代:大多数GC只发生在新生代,集中存放,提高缓存命中率,停顿时间短
老年代:Minorgc 15次以后还存活的升级到老年代
永久代:代码段区
对象分配------Minor GC(年龄计数器+1)。。。minor GC(第15次晋升)------老年代
代表语言:java
现在如果再有个面试官问你redis为什么这么快你知道该怎么说了吗?
- 纯内存操作
- 基于reactor的事件驱动高性能框架
- kv存储使用渐进式哈希解决哈希碰撞导致的查询变慢的问题同时避免了扩容时候的阻塞
- 利用了操作系统的零拷贝技术(持久化)和IO多路复用(网络)
- 重新封装了C语言的字符串,内存预分配 && 惰性释放,并将获取字符串长度优化到O(1)
- 单线程避免了锁的竞争
- string场景利用CPU缓存对于短key/value进行了优化
- 内存的惰性淘汰策略,减少了CPU的占用