1

我有一个具有 ConcurrentDictionary 作为私有成员的类。此类还定义了委托/回调方法。基类将此方法注册为外部事件的回调。这是只有一次。

我正在运行 ANT 内存分析器,我看到从数百个 ConcurrentDictionary 属性实例引用的 MyObj 实例有 1000 个。这些的 GC 根是事件回调。

这似乎导致内存在应用程序运行时显着增加。大约 5 分钟左右后,大部分内存被回收,但我担心应用程序可能会遇到问题,因为它会迅速膨胀在 GC 开始之前这么久。

这是怎么回事,我该如何解决?

这是注册处理程序的基本调用的片段

protected abstract void DataReceivedEventHandler(DataChangedEvent evt);

public virtual void RegisterForChanges(ICollection<MemoryTable> tables)
{
    foreach (MemoryTable table in tables)
    {
        _subscribedTables.Add(table);
        table.RegisterEventListener(new DataChangedCallBack(this.DataReceivedEventHandler));

    }
}

这是在上述基类的子类中实现的处理程序:

private ConcurrentDictionary<string, DataRecord> _cachedRecords;

protected override void DataReceivedEventHandler(DataChangedEvent evt)
{
    DataRecord record = evt.Record as DataRecord;
    string key = record.Key;
    if (string.IsNullOrEmpty(key)) { return; }

    if (_cachedRecords.ContainsKey(key))
    {
        _cachedRecords[key] = record;

        DateTime updateTime = record.UpdateTime;
        TimeSpan delta = updateTime - _lastNotifyTime;
        if (delta.TotalMilliseconds > _notificationFrequency)
        {
            PublishData(updateTime);
        }
    }
}

publishData 方法发布 prism 事件

4

5 回答 5

5

这是怎么回事

是的,事件是代表的列表,它有两个相关字段:targetmethod。除非你引用的是静态方法,否则target就是对类的引用。并且是告诉事件调用哪个方法method的反射。MemberInfo

如何排除故障

考虑在方法中放置一个断点add_EventName。(如果您没有显式的 add_EventName 和 remove_EventName,则必须使用此显式代码重新定义您的事件)。

  private event EventHandler eventName;
  public event EventHandler EventName
  {
     add { eventName += value; } // Breakpoint here
     remove { eventName -= value; }
  }

这将帮助您找到它被订阅这么多次的原因。

于 2012-06-01T15:44:26.877 回答
1

委托包含对对象的强引用以及对该对象调用什么方法的指示。因此,持有对委托的强引用的活动对象将保持委托将对其进行操作的对象(以及该对象持有强引用的任何对象)。

有时人们会希望注册一个回调或事件,以便为其他对象的利益而对一个对象进行操作,但回调或事件不应该仅仅为了它自己而使对象保持活动状态。例如,一个对象可能希望记录某个长期存在的对象引发特定事件的次数,从而创建一个附加到该事件的“事件计数器”对象的实例。只要长寿命对象持有计数器对象的事件订阅,该计数器对象将保持活动状态,并且每次引发事件时计数器都会递增。当然,如果曾经看过计数器的每个人都已不复存在,那么无论是计数器,还是增加它所需的努力,都不会起到任何有用的作用。

如果有一个回调,希望在未来某个确定的时间点被触发,但只有当一些实时引用(在回调本身之外)存在于它所操作的对象时,回调才会起到有用的作用,它可能将回调注册到转发对象很有用,如果这样做有意义,该对象又会将调用转发到主对象。实现这一点的最简单方法是让转发对象持有一个WeakReference. 当转发对象收到呼叫时,它会TargetWeakReference. 如果它不为空,它会将检索到的目标转换为主对象的类型并在其上调用适当的方法。如果主对象在回调执行之前不再存在,则WeakReference.Target属性将为空,转发对象的回调将简单地静默返回。

Target一些进一步的注意事项:(1)将 的 设置为委托并调用它可能很诱人WeakReference,但这种方法只有在真正的目标对象本身持有对该委托的引用时才有效;否则,即使它的目标不是,委托本身也将有资格进行垃圾收集;WeakReference(2) 将转换为接口并让主对象实现该接口可能会有所帮助。这将允许一个转发对象类与许多其他类一起使用。如果一个类可能想要附加到许多弱事件,使用通用接口可能会有所帮助:

接口 IDispatchAction<DummyType,ParamType>
{
  无效法(ParamType 参考参数);
}

这将允许主对象公开许多IDispatchAction操作(例如,如果一个类实现了IDispatchAction<foo,int>.Actand IDispatchAction<bar,int>.Act,则将该类的引用转换为这些接口之一并调用Act它会调用适当的方法)。

于 2012-06-01T16:05:05.650 回答
1

一次引发外部事件后,取消订阅您的类(SomeClass.SomeEvent -= MyEventHandler),或者您可以查看使用 Wea​​kReferences

于 2012-06-01T15:43:31.237 回答
0

您是否有可能一遍又一遍地重新订阅表格?我看到这个:

foreach (MemoryTable table in tables)
{
    _subscribedTables.Add(table);
    table.RegisterEventListener(new DataChangedCallBack(this.DataReceivedEventHandler));
}

我希望看到一个检查以确保表没有被重新订阅:

foreach (MemoryTable table in tables)
{
    if (!_subscribedTables.Contains(table)) {
        _subscribedTables.Add(table);
        table.RegisterEventListener(new DataChangedCallBack(this.DataReceivedEventHandler));
    }
}

编辑:鉴于问题开头的评论,我相当有信心问题(如果你可以称之为问题)就在这里:

if (_cachedRecords.ContainsKey(key))
{
    _cachedRecords[key] = record;

您在这里所说的是,如果记录的键已存在于 中cachedRecords,则将值替换为(可能)新行实例。这可能是因为某些后台进程导致行的数据发生更改,您需要将这些新值传播到 UI。

我的猜测是 MemoryTable 类正在为这些更改创建一个的 DataRecord 实例,并将该新实例沿事件链发送到我们在此处看到的处理程序。如果事件被触发了数千次,那么你当然会在内存中拥有数千次。垃圾收集器通常可以很好地清理这些东西,但您可能需要考虑就地更新以避免收集这些实例时发生的大量 GC。

您不应该的是尝试控制(甚至预测)GC 何时运行。只要确保在 GC 收集后,多余的对象都消失了(换句话说,确保它们没有被泄漏),你会没事的。

于 2012-06-01T16:52:06.850 回答
0

如果我们希望对象定义的回调方法不留在内存中,我们必须将此方法定义为静态。

于 2016-03-16T06:45:26.163 回答