【问题标题】:Force to clear memory强制清除内存
【发布时间】:2013-04-26 02:53:42
【问题描述】:

我的项目遇到了一些内存问题,因此我决定对某些部分进行压力测试以查看一些性能测量结果。我正在使用 Google 的 ConcurrentLinkedHashMap 库作为 LRU 内存缓存。我的测试代码的相关部分如下所示:

final ConcurrentLinkedHashMap cache = new ConcurrentLinkedHashMap.Builder<Long,Long>()
            .maximumWeightedCapacity(200000)
            .build();

for (int i = 0; i < 10; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            int count = 0;
            while (count < 1000000) {
                if (throttle) {
                    Thread.sleep(1000);
                    continue;
                }
                cache.put(random.nextLong(), random.nextLong());
                count++;
            }
        }
    }).start();
}

this.wait();

一旦内存达到 50% 以上,我将 throttle 标志设置为 true。我有一个监控线程,每隔2 秒进行一次测量。这是我得到的数字:

Size: 423902    Weighted Size: 200001   Memory: 0.11229571913729916
Size: 910783    Weighted Size: 200001   Memory: 0.25812696264655144
Size: 1373394   Weighted Size: 200001   Memory: 0.38996117352719034
Size: 2120239   Weighted Size: 200001   Memory: 0.6203725762957892
Size: 2114424   Weighted Size: 200000   Memory: 0.6233790564491212
Size: 2114424   Weighted Size: 200000   Memory: 0.6233790564491212
Size: 2114424   Weighted Size: 200000   Memory: 0.6233790564491212
Size: 2114424   Weighted Size: 200000   Memory: 0.6233790564491212
Size: 2114424   Weighted Size: 200000   Memory: 0.6233790564491212
Size: 2114424   Weighted Size: 200000   Memory: 0.6233790564491212

由于某种原因,我没有看到 LRU 缓存的 evicted 条目正在清理。我听说手动调用System.gc() 是个坏主意。如果是这样,有什么好方法可以有效清理内存?

旁注:有人知道size()ConcurrentLinkedHashMap 返回什么吗? weightedSize() 返回正确的值,但我担心 size 返回的值要大得多..

【问题讨论】:

  • 如何获取统计信息?例如:大小:423902 加权大小:200001 内存:0.11229571913729916 ...

标签: java memory guava out-of-memory concurrenthashmap


【解决方案1】:

正如您的数字所示,缓存并不严格保证最大值,但会尝试保持该高水位线。如果它确实做出了强有力的保证,那么它将通过对每个写入操作进行阻塞调用来限制并发性,并且无法有效地维护顶级 LRU 策略。在 Guava 的 Cache 中,我们确实通过使用锁定条带化来实现这一限制,但这种权衡带来了限制。

由于 CLHM 异步连接哈希表和 LRU 数据结构,这两个数字可能会发生偏差。 size 由修饰的ConcurrentHashMap 测量。这是在任何给定时刻对键值对的最准确估计。 weighted size 由 LRU 策略在针对其数据结构重放映射操作以确定新近排序并执行驱逐时计算。加权大小保持在预期的最大值,因为当超过阈值时策略将驱逐。

预计大小会在突发中有所不同,但缓存将始终尝试快速自我纠正。在您的示例中,由于操作系统会为较长的时间片安排线程,因此行为会更加严重。这允许它执行比实际场景中更多的插入。摊销的 LRU 管理意味着驱逐无法跟上,因为它在任何给定时刻从一个线程中窃取了一点时间。为了更好地模拟实际行为,MemoryLeakTest 强制在操作之间进行上下文切换。

如果您实施驱逐侦听器,您应该会看到计数增加并且您已达到稳定状态。如果您强制执行上下文切换,您应该会看到大小保持在下限。如果您想对地图中可能出现的条目总数进行更严格的限制,那么更喜欢 Guava 实现。

【讨论】:

  • 感谢您的意见。我知道测试有点极端,我的实际应用程序可能平均每秒只能执行 50 次插入。但是,我不明白为什么当我throttle 所有线程时(当应用程序检测到 >50% 的内存使用率时),JVM 没有利用这个机会“清除”从 LRU 映射中逐出的项目..
  • 或者,更确切地说,LRU 地图并没有利用这个机会来实际清除地图,因此 size() 将等于 weightedSize()
  • 实际上,接受这个作为答案。查看我的统计日志,我每秒执行约 500 次插入。但是,在每次插入之前添加 Thread.sleep(2) 似乎为缓存提供了足够的时间来自动更正自身。所以看来这不是我要找的问题。谢谢! (如果你对我之前关于为什么 LRU 缓存最终不会自行清除的问题有答案,请告诉我)
  • 请使用您的代码创建一个 github 存储库,我们可以通过电子邮件/等聊天。大小应强制为加权大小。与您的示例不同,缓存是以读取为中心的,因此大多数调整都是对写入持悲观态度,对读取持机会主义态度。这可能使它看起来比平常更时髦,但我必须深入挖掘和调查。
【解决方案2】:

默认的驱逐策略不是先进先出吗?如果是这样,您是否正在查看第一个插入以看到它们被驱逐?如果您正在查看最后一个,您将看不到它们。

这是来自文档:

默认 * weighter 为每个值分配 1 的权重,以通过 * 键值对的总数。包含集合的地图可以选择 * 按集合中元素的数量加权值并绑定地图 * 按其包含的元素总数。改变一个值 * 修改其权重需要对 * 地图。

所以默认情况下加权大小不会根据我的阅读而改变。

【讨论】:

  • 我不是,我只是假设 LRU 策略按规定工作。我只是随机生成要插入的 long 对象。我如何检查他们是否真的被驱逐了?我认为他们被驱逐了,因为weightedSize() 计数很好(200000)
  • 但是为什么你会期望加权大小会改变呢?一旦你点击它,当添加一项时,一项将被驱逐(根据策略)。所以加权大小将保持不变。
  • 没错,所以我假设由于weightedSize() 符合预期,那么之前的项目将按预期被驱逐。
猜你喜欢
  • 2017-05-19
  • 1970-01-01
  • 2011-05-02
  • 2016-02-18
  • 2014-12-23
相关资源
最近更新 更多