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

发布于 2024-10-02 04:40:59 字数 1429 浏览 0 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

街角卖回忆 2024-10-09 04:40:59

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

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

该解决方案包括以下内容:

  • 表达式包含对其依赖项的弱引用列表,并在发生更改时通知它们。这样就不会出现内存泄漏,也无需取消订阅。
  • 在表达式求值期间,它会以类似于我之前的答案中描述的方式自动检测依赖项并订阅它们。
  • 保留依赖项列表是为了防止过早对中间表达式进行垃圾收集(我之前的答案中的 SumProxyExpression 情况)。这样,每个弱引用都有其反向的强引用,这样弱引用链就不会被 GC 破坏,除非这些链无处可去。

If anyone is interested (which nobody probably is anyway), I had to lay off the global cache idea and solved the problem by making my Expressions self-caching.

I implemented all the logic in a base class called ExpressionBase.

The solution includes the following:

  • An expression contains a list of weak references to its dependants and notifies them on change. This way there are no memory leaks and no need to unsubscribe.
  • During an expression evaluation it automatically detects dependencies in a way similar to described in my previous answer and subscribes to them.
  • The list of dependencies is kept to prevent too early garbage-collection of intermediate expressions (the SumProxyExpression case from my previous answer). This way every weak reference has its reverse strong counterpart so that chains of weak references are not broken by GC unless these chains lead nowhere.
鹿港巷口少年归 2024-10-09 04:40:59

好的,在这里我将尝试使用 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 和表达式 a 之间的依赖关系列表b

核心代码

接下来,我通过全局缓存类提供了EvaluationContext,该类将缓存的数据存储在类似于WeakHashMap的结构中,并将依赖图数据存储在一个DAG 的节点类型为 WeakReference

这是我的evalupdate实现:

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);
    }
}

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

当表达式失效时,将探索依赖图并且所有依赖缓存都失效。

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

一切基本上都按其应有的方式进行。然而,也有一些棘手的情况。

棘手的情况

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

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();
    }
}

如果我缓存此类表达式的结果,它永远不会被垃圾收集,因为我保留对缓存值的硬引用(Result),并且它具有对包含类(表达式)的引用,因此表达式始终可达,但永远无法使用。

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

替代解决方案

我还考虑过通过从通用自缓存表达式类继承来实现它,而不是将所有内容都保存在全局缓存中。此解决方案将解决最后一个测试用例 (SelfReferencingExpression),但会因第一个测试用例 (SumProxyExpression) 而失败。

OK, here I will try to explain my approach to the problem using the Java language.

Everything will be explained on an example of SumExpression - the expression used to add the results of two other expressions together.

User code

I started with the most straightforward approach - the Observer pattern. Every expression would be listening to its dependencies for cache invalidation. Here is the version of SumExpression implemented in this way:

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);
    }
}

However, there is a whole lot of places where it can go wrong, and something as simple as addition of two numbers should be much more simple. So, I've decoupled the logic from caching in the following way:

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);
    }
}

Much simpler, huh? Note that here EvaluationContext's responsibility is twofold: it retrieves the values from cache and collects the list of dependencies between the SumExpression and expressions a and b.

Core code

Next, I provided EvaluationContext by the global caching class which stores the cached data in a structure similar to WeakHashMap<Expression, Object>, and the dependency graph data in a DAG with nodes being of type WeakReference<Expression>.

Here is my implementation of eval and update:

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);
    }
}

When my cache manager is asked for an object it first checks in cache. If there is no value in the cache it asks the expression to evaluate. The expression then asks the cache manager to resolve its dependencies by calling getValue() method. This creates an arc in the dependency graph. This graph is later used for cache invalidation.

When an expression is invalidated, the dependency graph is explored and all the dependent caches are invalidated.

Cache and dependency graph clean-up is performed as soon as the garbage collector notifies us (through ReferenceQueue) about the death of some expression objects.

Everything mostly works as it should. However, there are a few tricky cases.

Tricky cases

First case is a hanging intermediate dependency. Suppose we have the following class:

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);
    }
}

If we create an instance of c=SumProxyExpression(a,b) and change the value for a later we would want the c to change its value as well. However, if the intermediate SumExpression is already garbage-collected, this may not happen. To combat this, I do not delete nodes from the dependency graph unless they are leaf nodes (have only incoming or only outgoing arcs).

The other case, which I don't know how to solve, is the following:

class SelfReferencingExpression implements Expression<List<?>> {
    class Result extends ArrayList<Integer> {
    }

    @Override
    public List<?> evaluate(EvaluationContext resolver) {
        return new Result();
    }
}

If I cache the result of such an expression, it will never get garbage-collected because I keep hard references to cached values (Result), and it has a reference to a containing class (the expression), so the expression is always reachable, but could never be used.

This is a memory leak and I have no idea how to eliminate it. Telling user to never have such a reference is possible, but very dangerous, so I would like to find a better solution.

Alternative solutions

I also thought about implementing it with inheritance from a common self-caching expression class instead of holding everything in a global cache. This solution would solve the last test case (SelfReferencingExpression), but would fail with the first one (SumProxyExpression).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文