LRU 算法
LRU 全称是 Least Recently Used,即最近最久未使用的意思。
LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
LRU 接口设计
有了上面的设计原则,就可以得出设计一个带有 LRU 功能的缓存,应该具备以下功能:
1 | public interface LruCache<K, V> { |
说明:在实现 LRU 算法容器过程中,对参数校验笔者使用到了 guava 工具包:https://mvnrepository.com/artifact/com.google.guava/guava
1 | <dependency> |
实现1:利用 LinkedHashMap 实现
在 Java 中,LinkedHashMap 天然实现了 LRU 算法,因此我们可以设计一个 Cache 对象,内部自己维护一个 LinkedHashMap 容器,重写掉 removeEldestEntry() 方法即可。
1 | import com.google.common.base.Preconditions; |
为什么 LinkedHashMapLruCache 不直接继承 LinkedHashMap,而是在内部维护一个静态内部类呢?因为保证接口实现的“纯洁性”,LinkedHashMapLruCache 的使用者随便继承使用或直接使用,也只能使用上述定义的缓存接口中的方法,而不会使用到 LinkedHashMap 的方法。
上述这种实现很简单,完全依靠 LinkedHashMap 来维护缓存数据,重点在于内部类 InternalLruLinkedHashMapCache 一定要重写 removeEldestEntry() 方法,当达到容器容量满时,触发 LinkedHashMap 对最久访问数据的移除。
单元测试:
1 | import org.woodwhales.guava.lru.LruCache; |
日志输出:
1 | {1=1, 2=2, 3=3} |
日志输出结果符合预期。
实现2:利用 LinkedList + HashMap 实现
上面一种实现完全利用 Java 自带的 LinkedHashMap 容器实现,如果不允许使用 LinkedHashMap,则需要自己实现 LinkedHashMap 类似的功能:LinkedList + HashMap
使用 LinkedList 维护着缓存中元素的 key,保证key的顺序就可以真正的数据存放在 HashMap 中。
1 | import com.google.common.base.Preconditions; |
单元测试:
1 | import org.woodwhales.guava.lru.LinkedListLruCache; |
日志输出:
1 | {1=1,2=2,3=3} |
实现3:软引用缓存(增强版缓存)
上述两种实现的容器,缓存中的对象都是强引用,可能会存在一种极端情况:当开发者在内存中使用了大量的缓存,而这些缓存中的内容一旦“满员”,那么这个缓存容器就会维护着大量的强引用对象,这些对象会一直得不到垃圾回收。从而导致堆内存溢出(OOM)。
Java 中的引用有四种:强引用、软引用、弱引用、幻影(虚)引用。
软引用会在内存快要 OOM 的时候被 GC 回收。因此我们可以利用这个特性,对缓存中的数据做软引用,而不是使用强引用。
1 | import com.google.common.base.Preconditions; |
上述实现是在实现1的基础上做了一点点变动,即 LinkedHashMap 中存储的数据对象是 SoftReference
单元测试:
测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails
1 | import org.woodwhales.guava.lru.LinkedHashMapLruCache; |
上述单元测试中,由于设置了堆内存最大上限,因此在使用强引用的缓存时,堆内存会被缓存不断地占满,最终导致堆内存全部被占满,即每 500 毫秒创建 2MB 的内存在堆中,堆的最大容量是 128 MB,因此当循环到达 60 次左右的会出现 OOM。而软引用缓存中,当堆内存不够的时候,创建新的对象到堆的频率不够快,GC 来得及回收掉部分软引用对象,那么循环会一直进行下去,不会出现 OOM。