可靠地迫使番石榴地图被驱逐

编辑:我重新组织了这个问题,以反映自那以后可用的新信息。

这个问题是基于对Viliam关于Guava Maps使用懒惰驱逐的问题的回答:Guava地图中的驱逐懒惰

请首先阅读这个问题及其回答,但基本上结论是,番石榴地图不会异步计算并强制执行驱逐。 给出以下地图:

ConcurrentMap cache = new MapMaker() .expireAfterAccess(10, TimeUnit.MINUTES) .makeMap(); 

在访问条目后十分钟过后,在再次“触摸”地图之前,它仍然不会被驱逐。 已知的方法包括常用的访问器 – get()put()以及containsKey()

问题的第一部分[已解决]:其他调用会导致地图被“触摸”? 具体来说,有没有人知道size()属于这一类?

想知道这一点的原因是我已经实现了一个计划任务来偶尔轻推我用于缓存的Guava地图,使用这个简单的方法:

 public static void nudgeEviction() { cache.containsKey(""); } 

但是我也使用cache.size()以编程方式报告地图中包含的对象数量,以此确认此策略是否正常工作。 但我无法看到这些报告的差异,现在我想知道size()也会导致驱逐。

答:因此Mark指出在第9版中,只有get()put()replace()方法get()调用驱逐,这可以解释为什么我没有看到containsKey()的效果。 这显然会随着即将发布的下一版番石榴而改变,但遗憾的是我的项目发布时间越早。

这让我陷入了一个有趣的困境。 通常我仍然可以通过调用get("")来触摸地图,但我实际上正在使用计算地图:

 ConcurrentMap cache = new MapMaker() .expireAfterAccess(10, TimeUnit.MINUTES) .makeComputingMap(loadFunction); 

其中loadFunction从数据库加载与该键对应的MyObject 。 它开始看起来像我没有简单的方法强迫驱逐直到r10。 但是,即使能够可靠地强制驱逐,我的问题的第二部分仍然存在疑问:

我的问题的第二部分[已解决]: 针对相关问题的其中一个回复 ,是否触摸地图可靠地逐出所有过期的条目? 在链接的答案中, Niraj Tolia另有说明,称驱逐可能只是分批处理,这意味着可能需要多次触摸地图以确保所有过期的对象被驱逐。 他没有详细说明,但这似乎与基于并发级别的地图被拆分成相关。 假设我使用r10,其中containsKey("")确实调用驱逐,那么这将是整个地图,还是只针对其中一个分段?

答: maaartinus解决了这部分问题:

请注意containsKey和其他读取方法只运行postReadCleanup ,它在每次第64次调用时都不执行任何操作(请参阅DRAIN_THRESHOLD)。 此外,看起来所有清理方法仅适用于单个Segment。

所以看起来调用containsKey("")似乎不是一个可行的修复,即使在r10中也是如此。 这减少了我对标题的质疑: 我如何可靠地强制驱逐?

注意:我的Web应用程序明显受此问题影响的部分原因是,当我实现缓存时,我决定使用多个映射 – 每个映射一个数据对象。 所以有了这个问题,就有可能执行一个代码区域,导致一堆Foo对象被缓存,然后长时间不再触摸Foo缓存,所以它不会驱逐任何东西。 同时BarBaz对象正在从其他代码区域缓存,并且内存正在被吃掉。 我在这些地图上设置了最大尺寸,但这最好是一个脆弱的保护措施(我假设其影响是立即的 – 仍然需要确认这一点)。

更新1:感谢Darren将相关问题联系起来 – 他们现在有了我的投票。 所以它看起来像是一个解决方案,但似乎不太可能在r10中。 与此同时,我的问题仍然存在。

更新2:在这一点上,我只是在等待一个番石榴团队成员提供有关黑客maaartinus的反馈,我把它放在一起(见下面的答案)。

最后更新:收到反馈!

我刚刚将方法Cache.cleanUp()添加到Guava中。 从MapMaker迁移到CacheBuilder您可以使用它来强制驱逐。

我想知道您在问题的第一部分中描述的相同问题。 从我看到Guava的CustomConcurrentHashMap(第9版)的源代码可以看出,似乎在get()put()replace()方法中逐出了条目。 containsKey()方法似乎不会调用驱逐。 我不是百分百肯定,因为我快速通过了代码。

更新:

我还在Guava的git存储库中找到了一个更新版本的CustomConcurrentHashmap ,它看起来像containsKey()已被更新以调用驱逐。

版本9和我刚刚发现的最新版本在调用size()时不会调用驱逐。

更新2:

我最近注意到Guava r10 (尚未发布)有一个名为CacheBuilder的新类。 基本上这个类是MapMaker的分叉版本,但考虑到了缓存。 文档表明它将支持您正在寻找的一些驱逐要求。

我查看了r10版本的CustomConcurrentHashMap中的更新代码,发现了看起来像预定的地图清理器。 不幸的是,此代码似乎尚未完成,但r10每天看起来越来越有希望。

请注意containsKey和其他读取方法只运行postReadCleanup ,它在每次第64次调用时都不执行任何操作(请参阅DRAIN_THRESHOLD)。 此外,看起来所有清理方法仅适用于单个Segment。

强制驱逐的最简单方法似乎是将一些虚拟对象放入每个段。 为了实现这一点,您需要分析CustomConcurrentHashMap.hash(Object) ,这肯定不是一个好主意,因为此方法可能随时更改。 此外,根据密钥类,可能很难找到具有hashCode的密钥,以确保它落在给定的段中。

您可以使用读取,但每段需要重复64次。 在这里,很容易找到具有适当hashCode的密钥,因为这里允许任何对象作为参数。

也许你可以反而入侵CustomConcurrentHashMap源代码,它可能就像琐碎一样

 public void runCleanup() { final Segment[] segments = this.segments; for (int i = 0; i < segments.length; ++i) { segments[i].runCleanup(); } } 

但是如果没有大量的测试和/或番石榴团队成员的确定,我就不会这样做。

是的,我们已经来回几次关于这些清理任务是应该在后台线程(或池)上完成,还是应该在用户线程上完成。 如果它们是在后台线程上完成的,那么最终会自动发生; 事实上,它只会在每个细分受到使用时发生。 我们仍然试图在这里提出正确的方法 – 在未来的某个版本中看到这种变化我不会感到惊讶,但我也无法承诺任何事情,甚至不能对其如何改变做出可信的猜测。 不过,您已经为某种背景或用户触发的清理提供了合理的用例。

你的黑客是合理的,只要你记住它是一个黑客,并可能在未来的版本中打破(可能以微妙的方式)。 正如您在源代码中看到的那样,Segment.runCleanup()调用runLockedCleanup和runUnlockedCleanup:runLockedCleanup()如果无法锁定该段将无效,但如果它无法锁定该段,则因为某些其他线程具有段锁定,并且其他线程可以作为其操作的一部分调用runLockedCleanup。

此外,在r10中,还有CacheBuilder / Cache,类似于MapMaker / Map。 对于makeComputingMap的许多当前用户来说,Cache是​​首选方法。 它在common.cache包中使用单独的CustomConcurrentHashMap; 根据您的需要,您可能希望您的GuavaEvictionHacker同时使用它们。 (机制是相同的,但它们是不同的类,因此是不同的方法。)

在绝对必要之前,我不喜欢攻击或分析外部代码。 出现此问题的部分原因是由于MapMaker早期决定派生ConcurrentHashMap,因此拖延了很多复杂性,这些复杂性本来可以推迟到算法算出之后。 通过在MapMaker上方修补,代码对库更改很有用,因此您可以按照自己的计划删除变通方法。

一种简单的方法是使用弱引用任务和专用线程的优先级队列。 这具有创建许多陈旧的无操作任务的缺点,由于O(lg n)插入惩罚,这些任务可能变得过多。 它适用于小型,不常用的缓存。 这是MapMaker采用的原始方法,并且编写自己的装饰器很简单。

更强大的选择是使用单个到期队列镜像锁定摊销模型。 队列的头部可以是易失性的,因此读取总是可以查看它是否已经过期。 这允许所有读取触发过期,并允许定期检查清理线程。

到目前为止,最简单的方法是使用#concurrencyLevel(1)强制MapMaker使用单个段。 这会降低写入并发性,但大多数高速缓存读取都很重,因此损失很小。 用虚拟键轻推地图的原始黑客然后可以正常工作。 这将是我的首选方法,但如果你有很高的写入负载,其他两个选项都可以。

我不知道它是否适合您的用例,但您对缺少后台缓存逐出的主要担心似乎是内存消耗,所以我会想到在MapMaker上使用softValues()来允许垃圾收集器发生低内存情况时从缓存中回收条目。 很容易成为您的解决方案。 我在订阅服务器(ATOM)上使用它,其中通过使用SoftReferences值的Guava缓存来提供条目。

根据maaartinus的回答,我想出了以下代码,它使用reflection而不是直接修改源代码(如果你发现这个有用请请他回答!)。 虽然使用reflection会对性能造成损失,但差异应该可以忽略不计,因为我将为每个缓存Map每20分钟运行一次(我还在静态块中缓存动态查找,这将有所帮助)。 我做了一些初步测试,似乎按预期工作:

 public class GuavaEvictionHacker { //Class objects necessary for reflection on Guava classes - see Guava docs for info private static final Class computingMapAdapterClass; private static final Class nullConcurrentMapClass; private static final Class nullComputingConcurrentMapClass; private static final Class customConcurrentHashMapClass; private static final Class computingConcurrentHashMapClass; private static final Class segmentClass; //MapMaker$ComputingMapAdapter#cache points to the wrapped CustomConcurrentHashMap private static final Field cacheField; //CustomConcurrentHashMap#segments points to the array of Segments (map partitions) private static final Field segmentsField; //CustomConcurrentHashMap$Segment#runCleanup() enforces eviction on the calling Segment private static final Method runCleanupMethod; static { try { //look up Classes computingMapAdapterClass = Class.forName("com.google.common.collect.MapMaker$ComputingMapAdapter"); nullConcurrentMapClass = Class.forName("com.google.common.collect.MapMaker$NullConcurrentMap"); nullComputingConcurrentMapClass = Class.forName("com.google.common.collect.MapMaker$NullComputingConcurrentMap"); customConcurrentHashMapClass = Class.forName("com.google.common.collect.CustomConcurrentHashMap"); computingConcurrentHashMapClass = Class.forName("com.google.common.collect.ComputingConcurrentHashMap"); segmentClass = Class.forName("com.google.common.collect.CustomConcurrentHashMap$Segment"); //look up Fields and set accessible cacheField = computingMapAdapterClass.getDeclaredField("cache"); segmentsField = customConcurrentHashMapClass.getDeclaredField("segments"); cacheField.setAccessible(true); segmentsField.setAccessible(true); //look up the cleanup Method and set accessible runCleanupMethod = segmentClass.getDeclaredMethod("runCleanup"); runCleanupMethod.setAccessible(true); } catch (ClassNotFoundException cnfe) { throw new RuntimeException("ClassNotFoundException thrown in GuavaEvictionHacker static initialization block.", cnfe); } catch (NoSuchFieldException nsfe) { throw new RuntimeException("NoSuchFieldException thrown in GuavaEvictionHacker static initialization block.", nsfe); } catch (NoSuchMethodException nsme) { throw new RuntimeException("NoSuchMethodException thrown in GuavaEvictionHacker static initialization block.", nsme); } } /** * Forces eviction to take place on the provided Guava Map. The Map must be an instance * of either {@code CustomConcurrentHashMap} or {@code MapMaker$ComputingMapAdapter}. * * @param guavaMap the Guava Map to force eviction on. */ public static void forceEvictionOnGuavaMap(ConcurrentMap guavaMap) { try { //we need to get the CustomConcurrentHashMap instance Object customConcurrentHashMap; //get the type of what was passed in Class guavaMapClass = guavaMap.getClass(); //if it's a CustomConcurrentHashMap we have what we need if (guavaMapClass == customConcurrentHashMapClass) { customConcurrentHashMap = guavaMap; } //if it's a NullConcurrentMap (auto-evictor), return early else if (guavaMapClass == nullConcurrentMapClass) { return; } //if it's a computing map we need to pull the instance from the adapter's "cache" field else if (guavaMapClass == computingMapAdapterClass) { customConcurrentHashMap = cacheField.get(guavaMap); //get the type of what we pulled out Class innerCacheClass = customConcurrentHashMap.getClass(); //if it's a NullComputingConcurrentMap (auto-evictor), return early if (innerCacheClass == nullComputingConcurrentMapClass) { return; } //otherwise make sure it's a ComputingConcurrentHashMap - error if it isn't else if (innerCacheClass != computingConcurrentHashMapClass) { throw new IllegalArgumentException("Provided ComputingMapAdapter's inner cache was an unexpected type: " + innerCacheClass); } } //error for anything else passed in else { throw new IllegalArgumentException("Provided ConcurrentMap was not an expected Guava Map: " + guavaMapClass); } //pull the array of Segments out of the CustomConcurrentHashMap instance Object[] segments = (Object[])segmentsField.get(customConcurrentHashMap); //loop over them and invoke the cleanup method on each one for (Object segment : segments) { runCleanupMethod.invoke(segment); } } catch (IllegalAccessException iae) { throw new RuntimeException(iae); } catch (InvocationTargetException ite) { throw new RuntimeException(ite.getCause()); } } } 

我正在寻找关于这种方法是否可取作为权宜之计的反馈,直到问题在Guava版本中得到解决,特别是来自番石榴团队成员的一分钟。

编辑:更新解决方案以允许自动驱逐地图( NullConcurrentMapNullComputingConcurrentMap驻留在ComputingMapAdapter )。 事实certificate这在我的案例中是必要的,因为我在我的所有地图上都调用了这个方法,其中一些是自动驱逐器。