操作系统中的起源
缓存文件置换机制
现代语言的很多特性都可以在操作系统中找到最初的原型,LRU我们最早也可以在操作系统中找到当初的设计。
“高速缓存是计算机科学中唯一重要的思想” -Bill Joy
我们知道,无论是内存还是硬盘,又或者是我们在各自应用中用到的cache,由于大小固定,因而总会面临空间不足,而需要进行缓存置换(or替换),而替换的原则被我们称为缓存文件置换机制。
而今天聊得主题就是:最近最少未使用算法(LRU),即最久没有访问的内容作为替换对象。
页面置换算法
操作系统中,我们可以利用覆盖或交换来扩充内存。当操作系统的内存采用基本分页存储管理,即操作系统采用分页系统基础时,操作系统可以进行请求调页功能和页面置换功能,从而实现虚拟存储器的功能。选择调出页面的算法就成为页面置换算法。LRU就是其中一种。
最近最久未使用/LRU页面置换算法,选择最近最长时间未被访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。该算法为每个页面设置一个访问字段,来记录页面来自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。
访问页面 | 7 | 0 | 1 | 2 | 0 | 3 | 0 | 4 | 2 | 3 | 0 | 3 | 2 | 1 | 2 | 0 | 1 | 7 | 0 | 1 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
物理块1 | 7 | 7 | 7 | 2 | 2 | 4 | 4 | 4 | 0 | 1 | 1 | 1 | ||||||||
物理块2 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 3 | 3 | 0 | 0 | |||||||||
物理块3 | 1 | 1 | 3 | 3 | 2 | 2 | 2 | 2 | 2 | 7 | ||||||||||
缺页否 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
如上表所示,进程第一次对页面2进行访问时,将最近最久未被访问的页面7置换出去。随后访问页面3时,将最近最久未使用的页面1换出。
LruCache算法
前文介绍的LRU作为一种缓存淘汰策略,应用在cache上即LruCache 算法。
LruCache 算法通过 LinkedHashMap 来实现。LinkedHashMap继承于HashMap,它使用一个双向链表来存储 Map 中的 Entry 顺序关系,对 get、put、remove 等操作,LinkedHashMap 除了要做 HashMap 做的事情,还做些调整 Entry 顺序链表的工作。
LruCache 中将 LinkedHashMap 的顺序设置为 LRU 顺序来实现 LRU 缓存,每次调用 get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。 调用 put 插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。
Android中的LruCache
LruCache是Android 3.1(Api12)时引入,兼容低版本实际会使用v4包进行使用。LruCache的引入让系统在缓存不足时移除队列末尾的值,方便GC。LruCache用于内存缓存,在避免程序发生OOM和提高执行效率有着良好表现。
看一下androidx.collection包中的LruCache
:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
private int size; //当前缓存的大小
private int maxSize; //最大可缓存的大小
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount; //命中缓存的次数
private int missCount; //丢失缓存的次数
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
// ...
}
LruCache
的构造函数中传入的参数maxSize为可缓存的最大容量,maxSize
代表了LruCache
内部的LinkedHashMap
可存储的最大键值对数量。
public final V put(@NonNull K key, @NonNull V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
LruCache
的put()
方法首先判断了LruCache的键或值不能为null。
在插入元素前会调用一次safeSizeOf()
,safeSizeOf()
方法调用sizeOf()
方法,sizeOf()
默认返回1,一般会对它进行重写:比如LruCache存储的value是一个File,那么sizeOf
返回的就应该是当前对应该key的File的大小(所有的File大小不能超过maxSize)。因为当前缓存增加了,相应的size也要完成自增长,且将对应的key-value插入到链表中( 这一步:previous = map.put(key, value))。
if (previous != null) {entryRemoved(false, key, previous, value);}
这一步进行了二次检查,如果该key已经存在链表中,此时新的value覆盖后,size要减去之前的value所占用的大小。
为了保证多线程场景下size的准确性,用synchronized (this)
确保以上操作都是同步的。
如果是覆盖了旧的value,LruCache还对外提供了一个空方法entryRemoved。该方法会在一个值被remove或者put时被调用, 默认实现什么都不做。
trimToSize()
方法保证了缓存不溢出:
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
trimToSize()
方法会删除最旧的一条键值对,直到缓存<=最大容量。具体来说:每插入一次元素就会被调用该方法,方法是一个无限循环,当当前缓存大小不大于最大容量就结束循环。否则取出LinkedHashMap的entrySet的头部,也就是最早被插入且最近未被访问过的键值对并删除,更新size。重复此步骤直到缓存<=最大容量。LRU缓存的实现利用了访问顺序的LinkedHashMap的特性完成。
取值的get()
方法:
public final V get(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
get()
方法key值不能为null,如果map中存在与key相对应的value,则返回该value,并且缓存命中数+1。不存在,则缓存丢失数+1;如果不存在的话,会调用V createdValue = create(key)
去尝试根据该key创建一个value。create()
方法默认返回null,需要自己实现。
结尾
LruCache作为一个基础的缓存策略应用在很多方面,安卓从Android3.1引入,很多的开源框架比如Glide图片缓存中,也会用到LruCache。
参考
https://zh.wikipedia.org/w/index.php
https://developer.android.com/reference/android/util/LruCache