1.keep-alive的vue2和vue3的源码以及LRU算法
2.图解Linux内存回收之LRU算法(超级详细~)
3.深入源码解析LevelDB
4.LRU算法详解以及实现
5.FREE SOLO - 自己动手实现Raft - 17 - leveldb源码分析与调试-3
6.深入理解Linux内核内存回收逻辑和算法(LRU)
keep-alive的vue2和vue3的源码以及LRU算法
0.LRU算法
LRU(leastrecentlyused)根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰现阶段最久未被访问的数据
LRU的主体思想在于:如果数据最近被访问过,那么将来被访问的几率也更高
经典的LRU实现一般采用双向链表+Hash表。借助Hash表来通过key快速映射到对应的链表节点,然后进行插入和删除操作。这样既解决了hash表无固定顺序的源源码图鉴缺点,又解决了链表查找慢的缺点。
但实际上在js中无需这样实现,可以参考文章第三部分。先看vue的keep-alive实现。
1.keep-alivekeep-alive是vue中的内置组件,使用KeepAlive后,被包裹的组件在经过第一次渲染后的vnode会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的vnode进行渲染,并不需要再走一次组件初始化,render和patch等一系列流程,减少了script的执行时间,性能更好。
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive
从首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive属性判断是否需要缓存。
2.vue2的实现实现原理:通过keep-alive组件插槽,获取第一个子节点。根据include、exclude判断是否需要缓存,通过组件的key,判断是否命中缓存。利用LRU算法,更新缓存以及对应的keys数组。根据max控制缓存的最大组件数量。
先看vue2的实现:
exportdefault{ name:'keep-alive',abstract:true,props:{ include:patternTypes,exclude:patternTypes,max:[String,Number]},created(){ this.cache=Object.create(null)this.keys=[]},destroyed(){ for(constkeyinthis.cache){ pruneCacheEntry(this.cache,key,this.keys)}},mounted(){ this.$watch('include',val=>{ pruneCache(this,name=>matches(val,name))})this.$watch('exclude',val=>{ pruneCache(this,name=>!matches(val,name))})},render(){ constslot=this.$slots.defaultconstvnode:VNode=getFirstComponentChild(slot)constcomponentOptions:?VNodeComponentOptions=vnode&&vnode.componentOptionsif(componentOptions){ //checkpatternconstname:?string=getComponentName(componentOptions)const{ include,exclude}=thisif(//notincluded(include&&(!name||!matches(include,name)))||//excluded(exclude&&name&&matches(exclude,name))){ returnvnode}const{ cache,keys}=thisconstkey:?string=vnode.key==null?componentOptions.Ctor.cid+(componentOptions.tag?`::${ componentOptions.tag}`:''):vnode.keyif(cache[key]){ vnode.componentInstance=cache[key].componentInstance//makecurrentkeyfreshestremove(keys,key)keys.push(key)}else{ cache[key]=vnodekeys.push(key)//pruneoldestentryif(this.max&&keys.length>parseInt(this.max)){ pruneCacheEntry(cache,keys[0],keys,this._vnode)}}vnode.data.keepAlive=true}returnvnode||(slot&&slot[0])}}可以看到<keep-alive>组件的实现也是一个对象,注意它有一个属性abstract为true,产品定制源码是一个抽象组件,它在组件实例建立父子关系的时候会被忽略,发生在initLifecycle的过程中:
//忽略抽象组件letparent=options.parentif(parent&&!options.abstract){ while(parent.$options.abstract&&parent.$parent){ parent=parent.$parent}parent.$children.push(vm)}vm.$parent=parent然后在?created?钩子里定义了?this.cache?和?this.keys,用来缓存已经创建过的?vnode。
<keep-alive>直接实现了render函数,执行<keep-alive>组件渲染的时候,就会执行到这个render函数,接下来我们分析一下它的实现。
首先通过插槽获取第一个子元素的vnode:
constslot=this.$slots.defaultconstvnode:VNode=getFirstComponentChild(slot)<keep-alive>只处理第一个子元素,所以一般和它搭配使用的有component动态组件或者是router-view。
然后又判断了当前组件的名称和include、exclude(白名单、黑名单)的关系:
//checkpatternconstname:?string=getComponentName(componentOptions)const{ include,exclude}=thisif(//notincluded(include&&(!name||!matches(include,name)))||//excluded(exclude&&name&&matches(exclude,name))){ returnvnode}functionmatches(pattern:string|RegExp|Array<string>,name:string):boolean{ if(Array.isArray(pattern)){ returnpattern.indexOf(name)>-1}elseif(typeofpattern==='string'){ returnpattern.split(',').indexOf(name)>-1}elseif(isRegExp(pattern)){ returnpattern.test(name)}returnfalse}组件名如果不满足条件,那么就直接返回这个组件的vnode,否则的话走下一步缓存:
const{ cache,keys}=thisconstkey:?string=vnode.key==null?componentOptions.Ctor.cid+(componentOptions.tag?`::${ componentOptions.tag}`:''):vnode.keyif(cache[key]){ vnode.componentInstance=cache[key].componentInstance//makecurrentkeyfreshestremove(keys,key)keys.push(key)}else{ cache[key]=vnodekeys.push(key)//pruneoldestentryif(this.max&&keys.length>parseInt(this.max)){ pruneCacheEntry(cache,keys[0],keys,this._vnode)}}如果命中缓存,则直接从缓存中拿vnode的组件实例,并且重新调整了key的顺序放在了最后一个;否则把vnode设置进缓存,如果配置了max并且缓存的长度超过了this.max,还要从缓存中删除第一个。
这里的实现有一个问题:判断是否超过最大容量应该放在put操作前。为什么呢?我们设置一个缓存队列,都已经满了你还塞进来?最好先删一个才能塞进来新的。
继续看删除缓存的实现:
functionpruneCacheEntry(cache:VNodeCache,key:string,keys:Array<string>,current?:VNode){ constcached=cache[key]if(cached&&(!current||cached.tag!==current.tag)){ cached.componentInstance.$destroy()}cache[key]=nullremove(keys,key)}除了从缓存中删除外,还要判断如果要删除的缓存的组件tag不是当前渲染组件tag,则执行删除缓存的组件实例的$destroy方法。
————————————
可以发现,vue实现LRU算法是通过Array+Object,数组用来记录缓存顺序,Object用来模仿Map的功能进行vnode的缓存(created钩子里定义的this.cache和this.keys)
2.vue3的实现vue3实现思路基本和vue2类似,这里不再赘述。主要看LRU算法的实现。
vue3通过set+map实现LRU算法:
constcache:Cache=newMap()constkeys:Keys=newSet()并且在判断是君汇源码否超过缓存容量时的实现比较巧妙:
if(max&&keys.size>parseInt(maxasstring,)){ pruneCacheEntry(keys.values().next().value)}这里巧妙的利用Set是可迭代对象的特点,通过keys.value()获得包含keys中所有key的可迭代对象,并通过next().value获得第一个元素,然后进行删除。
3.借助vue3的思路实现LRU算法Leetcode题目——LRU缓存
varLRUCache=function(capacity){ this.map=newMap();this.capacity=capacity;};LRUCache.prototype.get=function(key){ if(this.map.has(key)){ letvalue=this.map.get(key);//删除后,再set,相当于更新到map最后一位this.map.delete(key);this.map.set(key,value);returnvalue;}return-1;};LRUCache.prototype.put=function(key,value){ //如果已经存在,那就要更新,即先删了再进行后面的setif(this.map.has(key)){ this.map.delete(key);}else{ //如果map中不存在,要先判断是否超过最大容量if(this.map.size===this.capacity){ this.map.delete(this.map.keys().next().value);}}this.map.set(key,value);};这里我们直接通过Map来就可以直接实现了。
而keep-alive的实现因为缓存的内容是vnode,直接操作Map中缓存的位置代价较大,而采用Set/Array来记录缓存的key来模拟缓存顺序。
参考:
LRU缓存-keep-alive实现原理
带你手撸LRU算法
Vue.js技术揭秘
原文;/post/图解Linux内存回收之LRU算法(超级详细~)
好文推荐:
全网最牛Linux内核分析--Intel CPU体系结构
一文让你读懂Linux五大模块内核源码,内核整体架构设计(超详细)
嵌入式前景真的好吗?那有点悬!
一文教你如何使用GDB+Qemu调试Linux内核
Linux内核必读五本书籍(强烈推荐)
全网独一无二Linux内核Makefle系统文件详解(一)(纯文字代码)
带你深度了解Linux内核架构和工作原理!
本文使用 Linux-2.6. 版本内核。
由于进程的内存空间分为多个段,如代码段、数据段、mmap段、堆段 和 栈段 等。那么,哪些段的内存会被交换到硬盘中呢? 答案就是:所有段的内存都有可能交换到硬盘。不过对于 代码段 和 mmap段 这些与文件有映射关系的内存区,只需要将数据写回到文件即可(由于代码段的内容不会改变,所以不用进行回写)。
文章福利小编推荐自己的Linux内核技术交流群: 整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前名进群领取,java报修源码额外赠送大厂面试题。
学习直通车:
内核资料直通车:
1. LRU 内存淘汰算法
把某个进程的匿名内存页 写入到 交换分区 后,进程又马上访问这个内存页,从而又要把这个内存页从 交换分区 中读入到内存中。这样只会增加系统的负荷,并且不能解决系统内存不足的问题。
LRU(Least Recently Used) 中文翻译是 最近最少使用 的意思,其原理就是:当内存不足时,淘汰系统中最少使用的内存,这样对系统性能的损耗是最小的。
2. LRU算法状态流转
转载地址: 图解 | Linux内存回收之LRU算法
深入源码解析LevelDB
深入源码解析LevelDB
LevelDB总体架构中,sstable文件的生成过程遵循一系列精心设计的步骤。首先,遍历immutable memtable中的key-value对,这些对被写入data_block,每当data_block达到特定大小,构造一个额外的key-value对并写入index_block。在这里,key为data_block的最大key,value为该data_block在sstable中的偏移量和大小。同时,构造filter_block,默认使用bloom filter,用于判断查找的key是否存在于data_block中,显著提升读取性能。meta_index_block随后生成,存储所有filter_block在sstable中的偏移和大小,此策略允许在将来支持生成多个filter_block,进一步提升读取性能。meta_index_block和index_block的偏移和大小保存在sstable的脚注footer中。
sstable中的block结构遵循一致的模式,包括data_block、夜袭源码专精index_block和meta_index_block。为提高空间效率,数据按照key的字典顺序存储,采用前缀压缩方法处理。查找某一key时,必须从第一个key开始遍历才能恢复,因此每间隔一定数量(block_restart_interval)的key-value,全量存储一个key,并设置一个restart point。每个block被划分为多个相邻的key-value组成的集合,进行前缀压缩,并在数据区后存储起始位置的偏移。每一个restart都指向一个前缀压缩集合的起始点的偏移位置。最后一个位存储restart数组的大小,表示该block中包含多少个前缀压缩集合。
filter_block在写入data_block时同步存储,当一个new data_block完成,根据data_block偏移生成一份bit位图存入filter_block,并清空key集合,重新开始存储下一份key集合。
写入流程涉及日志记录,包括db的sequence number、本次记录中的操作个数及操作的key-value键值对。WriteBatch的batch_data包含多个键值对,leveldb支持延迟写和停止写策略,导致写队列可能堆积多个WriteBatch。为了优化性能,写入时会合并多个WriteBatch的batch_data。日志文件只记录写入memtable中的key-value,每次申请新memtable时也生成新日志文件。
在写入日志时,对日志文件进行划分为多个K的文件块,每次读写以这样的每K为单位。每次写入的日志记录可能占用1个或多个文件块,因此日志记录块分为Full、First、Middle、Last四种类型,读取时需要拼接。
读取流程从sstable的层级结构开始,0层文件特别,可能存在key重合,因此需要遍历与查找key有重叠的所有文件,文件编号大的优先查找,因为存储最新数据。非0层文件,一层中的文件之间key不重合,利用版本信息中的元数据进行二分搜索快速定位,仅需查找一个sstable文件。
LevelDB的sstable文件生成与合并管理版本,通过读取log文件恢复memtable,仅读取文件编号大于等于min_log的日志文件,然后从日志文件中读取key-value键值对。
LevelDB的LruCache机制分为table cache和block cache,底层实现为个shard的LruCache。table cache缓存sstable的索引数据,类似于文件系统对inode的缓存;block cache缓存block数据,类似于Linux中的page cache。table cache默认大小为,实际缓存的是个sstable文件的索引信息。block cache默认缓存8M字节的block数据。LruCache底层实现包含两个双向链表和一个哈希表,用于管理缓存数据。
深入了解LevelDB的源码解析,有助于优化数据库性能和理解其高效数据存储机制。
LRU算法详解以及实现
1. LRU算法,作为缓存淘汰策略的佼佼者,其宗旨是优先清除那些久未触碰的条目。在内存管理及数据结构领域,它占据了核心地位。
2. 让我们通过单链表的协助,一步步解析LRU的运作机制。首先,我们有必要熟悉单链表的基础知识。
3. LRU算法利用单链表的特点,新数据插入时总是放在表头,命中缓存时则将其移到表头,当达到容量上限时,表尾的数据则自动被淘汰。
4. 熟练掌握链表的增删改查操作是实现LRU的先决条件。
5. 为了实现LRU,我们需要从零开始构建自定义的单链表。首先定义LinkList类,包含一个节点类Node,每个节点包含数据(T)和指向下一个节点的指针。
6. 接下来,编写LinkList的构造函数,分为有参和无参两种,以应对不同的使用场景。
7. 然后是LinkList的操作方法:put方法负责将数据插入链表头部,新节点会尽可能接近链表头部;remove方法用于移除节点,分为删除头部、任意位置和尾部;set方法用于修改链表中任意位置的数据,get方法则是通过索引来获取数据。
8. 有了链表的基础,我们继续实现LRU算法。创建LRULinkList类,它继承自LinkList,并增加内存限制DEFAULT_MEMORY和内存大小变量。
9. 在LRULinkList类中实现lruPut方法,当插入新数据时,如果内存已满,则先删除尾部数据再插入;如果未满,则直接插入。
. lruRemove方法简单调用父类的removeLast方法来处理尾部数据的淘汰。
. lruGet方法则是在访问数据时,将访问的节点移到链表头部,确保了最近使用的数据不会被淘汰。
. 通过以上步骤,我们构建了一个基本的LRU缓存实现,它能高效地管理数据,按照访问频率进行淘汰。
. 实现完成后,我们需要进行充分的测试,以验证算法的正确性和效率。
. 如果有任何疑问或需要源码,欢迎留言交流。
. 此外,如果你对Linux内核技术感兴趣,可以加入我们的技术交流群,获取更多学习资料和资源,共同探索内核世界的奥秘。
. 学习之旅永无止境,从LRU算法出发,在数据结构与内存管理的交织中,不断提升编程技能。
. 祝你在这次探索之旅中有所收获!
FREE SOLO - 自己动手实现Raft - - leveldb源码分析与调试-3
leveldb的数据流动路径是单向的,从内存中的memtable流向不可变的memtable,最终写入到磁盘上的sorted table文件中。以下是几个关键状态的分析,来了解内存和磁盘上数据的分布。
以下是分析所涉及的状态:
1. 数据全在内存中
随机写入条数据,观察到数据全部存储在memtable中,此时还没有进行compaction操作。
2. 数据全在磁盘中
写入大量数据,并等待数据完全落盘后重启leveldb。此时,数据全部存储在磁盘中,分布在不同的level中。在每个level的sstable文件中,可以看到key的最大值与最小值。
3. 数据部分在内存中,部分在磁盘中
随机写入条数据,发现内存中的memtable已满,触发compaction操作,数据开始写入到sstable文件。同时,继续写入的数据由于还未达到memtable上限,仍然保存在内存中。
4. 总结
通过观察不同数据写入量导致的数据在内存与磁盘间的流动,我们可以看到leveldb内部状态的转换。
下篇文章将分析LRUCache数据状态的变化。敬请期待!
深入理解Linux内核内存回收逻辑和算法(LRU)
在Linux内存管理中,LRU链表策略扮演着核心角色。操作系统通过active链表和inactive链表来跟踪内存页面的活跃程度,活跃页面在链表的前端,不活跃页面则位于后端。当页面被访问,活跃页面PG_active位会被置位,而PG_referenced位记录页面最近是否被访问。若长时间未被访问,PG_referenced位会被清除,进而可能从active链表转移到inactive链表。
struct zone中包含这些链表的关键字段,用于在活跃和不活跃页面之间移动。Linux通过PG_active和PG_referenced标志判断页面状态,避免过度依赖单一标志导致的活跃状态误判。Linux 2.6版本中,这两个标志在特定条件下协同工作,如页面频繁访问则从inactive链表移到active链表,反之则可能被清除。
页面移动的关键操作由函数如mark_page_accessed(), page_referenced(), activate_page()和shrink_active_list()执行。为减少锁竞争,Linux引入LRU缓存,通过pagevec结构批量管理页面,当缓存满时,页面才会一次性添加到链表中。
PFRA处理多种来源的页面,涉及free_more_memory()和try_to_free_pages()等函数。页面回收分为内存不足触发和后台进程kswapd定期操作,前者通过try_to_free_pages()检查并回收最不常用的页面,后者则通过balance_pgdat()调用shrink_zone()和shrink_slab()进行回收。
shrink_zone()的核心功能是将活跃页面移至不活跃链表,并回收不活跃链表中的页面。shrink_slab()则针对磁盘缓存页面的回收,通过shrinker函数进行处理。这些函数的源代码实现细节丰富,涉及内存分配和回收的复杂逻辑。