摘要
guava的緩存相信很多人都有用到,
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(100, TimeUnit.SECONDS) .maximumSize(10).build();
也常用的方法是設置過期時間。但使用過程中會遇到一些問題:當過期時間到了,緩存中的對象真的會立即被釋放嗎?當緩存達到容量以后,如何高效的剔除緩存?guava cache的底層數據結構是如何的?帶着這些問題,一起來看看guava cache的源碼
介紹一下guava緩存的基本框架
- LoacalCache:實現了currentMap接口,保存了一些配置信息,例如失效時間、容量等。是保存所有緩存最外層的容器
- segment:為了高並發,借鑒了currentMap中的分段鎖機制,segment可以理解是LocalCache中的一部分,不同的segment之間並發不受影響。每次操作根據key進行hash,保證了同一個key的put和set都在同一個segment中。segment中還有兩個分別隊列用於保存軟引用或者弱引用對象回收后的引用
- refrenceEntry:保存一個緩存key-val的對象,類似map中的entry,只不過map中entry保存的對象的直接進行,而refrenceEntry這是在中間多了一層valueReference
- valueReference:如果是強引用,則直接保存對象的直接引用,當然也可以使用軟引用的方法。
其實通過和CurrentHashMap最類比比較好理解,只不過guava緩存在其基礎上增強了緩存過期的機制:
- 最大對象個數限制
- 超時機制
- 弱引用或者軟引用
guava會oom嗎
答案是肯定的,當我們設置緩存用不過期(或者很長),緩存的對象不限個數(或者很大),例如
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(100000, TimeUnit.SECONDS) .build();
不斷向guava加入緩存大字符串,最終將能oom,解決這種辦法:
使用弱引用或者軟應用
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .weakValues().build();
guava在創建對象放在一個map(LocalCache.class)的時候,默認使用強引用(StrongValueReference.class),如果指定使用弱引用的時候,就會創建的是(WeakValueReference.class)
合適最大容量
這個也是比較推薦的方法,根據業務需求,設置合適的緩存容量、這樣超過容量以后,緩存就會按照LRU的方式回收緩存。
CacheBuilder.maximumSize(10)
guava緩存到期就會立即清除嗎
guava清楚過期緩存的機制是什么,是單獨使用線程來掃描嗎?不是的,是在每次進行緩存操作的時候,如get()或者put()的時候,判斷緩存是否過期。核心代碼
void expireEntries(long now) { drainRecencyQueue(); //多線並發的情況下,防止誤刪access ReferenceEntry<K, V> e; while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } }
其中 writeQueue是保存按照寫入緩存先后時間的隊列,每次get或者put都可能觸發觸發這個方法。accessQueue同理,對應的是最后訪問失效時間的功能。
因此可以看出,一個如果一個對象放入緩存以后,不在有任何緩存操作(包括對緩存其他key的操作),那么該緩存不會主動過期的。不過這種情況是極端情況下才會出現。
guava如何找出最久未使用的緩存
在上面也說到了,是用accessQueue,這個隊列的實現比較復雜。這個隊列其實是按照最久未使用的順序存放的緩存對象(ReferenceEntry)的。由於會經常進行元素的移動,例如把訪問過的對象放到隊列的最后。ReferenceEntry這個在前面框架圖里面說到了,使用來保存key-val的,其中接口包含一些特殊方法:
@Override public ReferenceEntry<K, V> getNextInAccessQueue() { throw new UnsupportedOperationException(); } @Override public void setNextInAccessQueue(ReferenceEntry<K, V> next) { throw new UnsupportedOperationException(); } @Override public ReferenceEntry<K, V> getPreviousInAccessQueue() { throw new UnsupportedOperationException(); } @Override public void setPreviousInAccessQueue(ReferenceEntry<K, V> previous) { throw new UnsupportedOperationException(); }
這樣通過ReferenceEntry可以就可以判斷該entry的后面節點,如果不在隊列中,則返回一個NullEntry的對象。這樣做的好處就彌補了 鏈表的缺點
- 判斷一個ReferenceEntry是否在隊列中,只要判斷該ReferenceEntry的前一個引用是否是NullEntry,不需要便利整個鏈表
並且可以很方便的更新和刪除鏈表中的節點,因為每次訪問的時候都可能需要更新該鏈表,放入到鏈表的尾部,這樣,每次從access中拿出的頭節點就是最久未使用的。 並且,如果按照訪問時間來刪除緩存的時候,只要從隊列里找出第一個訪問沒有超時的對象,那么之前遍歷的緩存都是應該刪除的,這樣就不需要遍歷整個緩存的對象來判斷。
對應的writeQueue用來保存最久未更新的緩存隊列,實現方式和accessQueue一樣。
總結
可以看出,guava緩存的原型是CurrentHashMap,在其基礎上考慮如果判斷緩存是否過期。底層的一些數據結構也是用的十分巧妙。如果能仔細的看看源碼,相信對你也有一定的幫助