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.
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。
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();
}
}
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).
发布评论
评论(2)
如果有人感兴趣(无论如何可能没有人感兴趣),我不得不放弃全局缓存的想法,并通过使我的
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
Expression
s self-caching.I implemented all the logic in a base class called
ExpressionBase
.The solution includes the following:
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.好的,在这里我将尝试使用 Java 语言来解释我的解决问题的方法。
所有内容都将通过 SumExpression 示例进行解释 - 该表达式用于将其他两个表达式的结果相加。
用户代码
我从最直接的方法开始——观察者模式。每个表达式都会监听其依赖项以判断缓存失效。以下是以此方式实现的 SumExpression 版本:
但是,有很多地方可能会出错,而像将两个数字相加这样简单的事情应该要简单得多。因此,我通过以下方式将逻辑与缓存分离:
简单得多,是吗?请注意,这里
EvaluationContext
的职责是双重的:它从缓存中检索值并收集SumExpression
和表达式a
之间的依赖关系列表b
。核心代码
接下来,我通过全局缓存类提供了
EvaluationContext
,该类将缓存的数据存储在类似于WeakHashMap
的结构中,并将依赖图数据存储在一个DAG 的节点类型为WeakReference
。这是我的eval和update实现:
当我的缓存管理器被要求提供一个对象时,它首先检查缓存。如果缓存中没有值,它会要求表达式进行计算。然后,表达式通过调用 getValue() 方法要求缓存管理器解决其依赖关系。这会在依赖图中创建一条弧。该图稍后用于缓存失效。
当表达式失效时,将探索依赖图并且所有依赖缓存都失效。
一旦垃圾收集器通知我们(通过 ReferenceQueue)某些表达式对象的死亡,就会立即执行缓存和依赖图清理。
一切基本上都按其应有的方式进行。然而,也有一些棘手的情况。
棘手的情况
第一种情况是悬挂的中间依赖项。假设我们有以下类:
如果我们创建
c=SumProxyExpression(a,b)
的实例并稍后更改a
的值,我们将需要c
也可以更改其值。但是,如果中间SumExpression
已被垃圾回收,则可能不会发生这种情况。为了解决这个问题,我不会从依赖图中删除节点,除非它们是叶节点(仅具有传入或仅传出弧)。我不知道如何解决的另一种情况如下:
如果我缓存此类表达式的结果,它永远不会被垃圾收集,因为我保留对缓存值的硬引用(
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:
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:
Much simpler, huh? Note that here
EvaluationContext
's responsibility is twofold: it retrieves the values from cache and collects the list of dependencies between theSumExpression
and expressionsa
andb
.Core code
Next, I provided
EvaluationContext
by the global caching class which stores the cached data in a structure similar toWeakHashMap<Expression, Object>
, and the dependency graph data in a DAG with nodes being of typeWeakReference<Expression>
.Here is my implementation of eval and update:
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:
If we create an instance of
c=SumProxyExpression(a,b)
and change the value fora
later we would want thec
to change its value as well. However, if the intermediateSumExpression
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:
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).