相互依赖的缓存失效和内存管理

时间:2010-11-14 11:46:02

标签: caching architecture memory-leaks invalidation

我正在开发一个Java项目,该项目大量使用Observer模式来确保每个数据对象状态都是最新的。我厌倦了保持这种混乱,并试图实现一个解决方案,将Observer模式的恐怖与我宝贵的数据对象分离。

我能够抽象出这个项目的细节,说我想解决的问题如下:

  

存在一组表示表达式的对象,每个对象都可以依赖于其他表达式的值。

     

需要以下两项操作:

     

eval():检索给定表达式的值

     

如果现在重新评估所有表达式依赖项,则此操作应返回表达式的最新值。但是,除非第二个操作使缓存失效,否则不应多次计算任何表达式:

     

update():修改给定的表达式

     

此操作使表达式的缓存以及直接或传递依赖于它的所有当前缓存表达式无效。

     

此外,还需要一些方便的内存泄漏方式来管理表达式的生命周期。

伪代码中的所需用法示例:

Expression a = variable(1);
Expression b = variable(3);
Expression s = sum(a,b);
assert(4 == eval(s));    // causes evaluation of expressions a, b and s
assert(4 == eval(s));    // does not cause any evaluations,
                         //     the result should be taken from cache
setValue(a,2);           // contains update() internally, 
                         //     invalidating caches for a and s
assert(5 == eval(s));    // causes evaluation of a and s

好的,功能部分结束了,这里是内存管理部分。

开发人员必须有一些简单的方法来管理表达式图。理想情况下,分配应该使用new Sum(a,b)完成,开发人员应该可以自由地传递表达式实例,而不需要太多的缓存知识,并且释放应该自动发生,而不需要开发人员的努力。

并且一定不能有任何内存泄漏。也就是说,当一个表达式被释放时,与它相关联的内存中不能有任何东西。例如,如果要将观察者模式用于失效,则必须从所有观察者列表中删除表达式。

问题是:

您最喜欢用自己喜欢的语言实现这一目标的方法是什么?

非垃圾收集和函数式语言也是受欢迎的,特别是功能性的,因为我根本不了解如何在纯函数中解决这个问题。

从我的观点来看,最好的解决方案是开发人员出错的可能性最小。

我故意不发布我当前的实现细节,因为我认为我在实现中发现了一个根本性的缺陷,我没有看到任何解决方法。我稍后会发布它。

2 个答案:

答案 0 :(得分:1)

如果有人感兴趣(无论如何都没有人),我不得不放下全局缓存的想法并通过Expression自我缓存来解决问题。

我在名为ExpressionBase的基类中实现了所有逻辑。

解决方案包括以下内容:

  • 表达式包含对其依赖项的弱引用列表,并在更改时通知它们。这样就没有内存泄漏,也没有必要取消订阅。
  • 在表达式评估期间,它会以与我之前的答案中描述的方式类似的方式自动检测依赖关系并订阅它们。
  • 保留依赖项列表以防止过早的中间表达式垃圾收集(SumProxyExpression案例来自我之前的答案)。这样每个弱引用都具有相反的强引用,因此除非这些链无处可去,否则不会破坏弱引用链。

答案 1 :(得分:0)

好的,在这里,我将尝试使用Java语言解释我对该问题的处理方法。

所有内容都将在SumExpression的示例中进行解释 - 该表达式用于将两个其他表达式的结果一起添加。

用户代码

我从最简单的方法开始 - 观察者模式。每个表达式都会监听其缓存失效的依赖关系。以下是以这种方式实现的SumExpression版本:

public class SumExpression implements Expression<Integer> {
    private final Expression<Integer> a;
    private final Expression<Integer> b;

    Integer value;
    private Listener invalidator = new Listener() {
        @Override
        public void changed() {
            invalidate();
        }
    };

    public SumExpression(SimpleVariable<Integer> a, SimpleVariable<Integer> b) {
        this.a = a;
        this.b = b;
        a.listeners().addListener(invalidator);// don't forget to call it!
        b.listeners().addListener(invalidator);
    }

    public Integer getValue()
    {
        validate();
        return value;
    }

    private void validate() {
        if(value == null)
            value = evaluate;
    }

    private void evaluate() {
        value = null;
    }

    public void dispose() { // USER, DON'T FORGET TO CALL IT!!!
        a.removeListener(invalidator);
        b.removeListener(invalidator);
    }

    ListenerCollection listeners = new ListenerCollection();

    @Override
    public void addListener(Listener l) {
        listeners.addListener(l);
    }

    @Override
    public void removeListener(Listener l) {
        listeners.removeListener(l);
    }
}

然而,有很多地方可能出错,而且添加两个数字之类的简单事情应该更加简单。所以,我已经通过以下方式将逻辑从缓存中分离出来了:

public class SumExpression implements Expression<Integer> {
    private final Expression<Integer> a;
    private final Expression<Integer> b;

    public SumExpression(Expression<Integer> a, Expression<Integer> b)
    {
        this.a = a;
        this.b = b;
    }

    public Integer evaluate(EvaluationContext context)
    {
        return context.getValue(a)+context.getValue(b);
    }
}

更简单,是吧?请注意,这里EvaluationContext的责任是双重的:它从缓存中检索值并收集SumExpression和表达式ab之间的依赖关系列表。

核心代码

接下来,我提供了EvaluationContext全局缓存类,它将缓存数据存储在类似于WeakHashMap<Expression, Object>的结构中,而依赖关系图数据存储在DAG中,节点类型为{{1} }。

以下是 eval 更新的实施方式:

WeakReference<Expression>

当我的缓存管理器被要求输入对象时,它首先检查缓存。如果缓存中没有值,则要求表达式进行求值。然后表达式要求缓存管理器通过调用getValue()方法来解析其依赖关系。这会在依赖关系图中创建一个弧。此图表稍后用于缓存失效。

当表达式无效时,将探索依赖关系图,并使所有相关缓存失效。

一旦垃圾收集器通知我们(通过ReferenceQueue)有关某些表达式对象的死亡,就会执行缓存和依赖关系图清理。

一切都是按照应有的方式运作。但是,有一些棘手的案例。

棘手的案件

第一种情况是悬挂中间依赖。假设我们有以下类:

public <T1> T1 eval(final Expression<T1> expression)
{
    Weak weak = weaken(expression);
    T1 result = (T1) cache.get(weak);
    if(result == null) {
        result = expression.evaluate(new EvaluationContext()
        {
            @Override
            public <T2> T2 getValue(Expression<T2> dependency) {
                registerDependency(expression, dependency);
                return eval(dependency);
            }
        });
        cache.put(weak, result);
    }
    return result;
}

public void update(Expression<?> ex) {
    changed(weaken(ex));
}

public void changed(Weak weak) {
    cache.remove(weak);

    dependencies.removeOutgoingArcs(weak);
    for(Weak dependant : new ArrayList<Weak>(dependencies.getIncoming(weak))) {
        changed(dependant);
    }
}

如果我们创建class SumProxyExpression implements Expression<Integer> { private final Expression<Integer> a; private final Expression<Integer> b; public SumProxyExpression(Expression<Integer> a, Expression<Integer> b) { this.a = a; this.b = b; } @Override public Integer evaluate(EvaluationContext context) { Expression<Integer> s = new SumExpression(a, b); return context.getValue(s); } } 的实例并稍后更改c=SumProxyExpression(a,b)的值,我们希望a也更改其值。但是,如果中间c已经被垃圾收集,则可能不会发生这种情况。为了解决这个问题,我不会从依赖关系图中删除节点,除非它们是叶节点(只有传入或仅传出弧)。

另一个我不知道如何解决的案例如下:

SumExpression

如果我缓存这样一个表达式的结果,它将永远不会被垃圾收集,因为我保持对缓存值(class SelfReferencingExpression implements Expression<List<?>> { class Result extends ArrayList<Integer> { } @Override public List<?> evaluate(EvaluationContext resolver) { return new Result(); } } )的硬引用,并且它引用了一个包含类(表达式),所以表达式总是可以访问的,但永远不能使用。

这是内存泄漏,我不知道如何消除它。告诉用户从来没有这样的参考是可能的,但非常危险,所以我想找到一个更好的解决方案。

替代解决方案

我还考虑过从公共自缓存表达式类继承实现它,而不是将所有东西都保存在全局缓存中。此解决方案将解决最后一个测试用例(SelfReferencingExpression),但会在第一个测试用例(SumProxyExpression)中失败。所以,我不知道该怎么做。请帮忙。