You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

树构建算法内存泄漏问题排查与优化求助

问题描述

实现了类操作系统目录结构的树构建算法,核心代码及使用方式如下:

TreeNode 核心实现

public class TreeNode
{
    protected TreeNode(string part)
        : this(part, -1, null)

    private TreeNode(
       string part, 
       int level, 
       TreeNode? parent)
    {
        Part = part;
        Level = level;
        Parent = parent;
        Root = parent?.Root ?? this;
    }

    private bool _updateEnqueued;
    public string Part {get;set;}
    public int Level {get;}
    public TreeNode? Parent {get;}
    public TreeNode Root {get;}

    public bool Add(
      string[] subTopic,
      MqttDelivery delivery)
      => Add(subTopic, Level + 1, delivery);

    public void ForceResync()
      => Root.Update();

    public void EnqueueUpdate()
    {
       if(_updateEnqueued)
          return;
 
       _updateEnqueued = true;
       Dispatcher.UIThread.Post(
           Update, 
           DispatcherPriority.Background);
    }

    private bool Add(string[] topic, int level, MqttDelivery delivery)
    {
       if(level >= topic.Length)
       {
          Topic = delivery.Topic;
          Header = delivery.Header?.ToJsonString(true);
          Payload = delivery.Payload?.ToJsonString(true);
          Delivery = delivery;

          AddToHistory(delivery);
          return false;
       }
       
       var part = topic[level];
       if(!_treeChildren.Value.TryGetValue(part, out var trie))
       {
          TopicsCount++;
          _treeChildren.Value.Add(part, trie = new TreeNode(part,level,this));
       }

       MessagesCount++;
       trie.Add(topic, level + 1, delivery);
       return true;
    }

    private void AppendItems()
    {
       _updateEnqueued = false;
       var flatTreeList = new AvaloniaList<TreeNode>();
       AppendItems(flatTreeList, this);
       _visibleChildren = flatTreeList;
    }
    
    private void AppendItems(
        AvaloniaList<TreeNode> flatTreeList,
        TreeNode treeNode)
   {
      flatTreeList.Add(treeNode);
      if(!treeNode.IsExpanded)
         return;

      foreach(var ch in treeNode._treeChildren.Value)
         AppendItems(flatTreeList, ch.Value);
   } 

   public void Update()
   {
      AppendItems();
      Root.RaisePropertyChanged(nameof(Root.VisibleChildren));
   }

   public void AddToHistory(MqttDelivery mqttMessage)
   {
      while(UiHistoryMessages.Count >= 30)
         UiHistoryMessages.RemoveAt(UiHistoryMessages.Count - 1);

      UiHistoryMessages.Insert(0, new ReceivedHistoryMessage{..});
   }
}

TreeRoot 实现

public class TreeRoot
    : TreeNode
{
    public TreeRoot()
        : base("")
    {
        var timer = new Timer(1000);
        timer.Elapsed += OnTimerElapsed;
        timer.Start();
    }

    private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
    {
        Root.EnqueueUpdate();
    }
    
}

使用方式

var root = new TreeRoot {IsExpanded = false};
root.ForceResync();

当前问题:持续向树中添加节点后,应用内存从80MB逐步增长至30分钟后的400MB,且无法自动释放。此时节点数约2000个,所有节点的UiHistoryMessages总数约10000条,数据量不大但内存无法回收。


内存泄漏排查与优化方案

1. Timer 未释放导致的内存泄漏

TreeRoot中创建的Timer未正确释放,且Elapsed事件持有对TreeRoot的强引用,导致整个树结构无法被GC回收。

  • 优化方案:
    让TreeRoot实现IDisposable接口,在Dispose方法中清理Timer:
    public class TreeRoot : TreeNode, IDisposable
    {
        private Timer? _timer;
        
        public TreeRoot() : base("")
        {
            _timer = new Timer(1000);
            _timer.Elapsed += OnTimerElapsed;
            _timer.Start();
        }
    
        private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
        {
            Root.EnqueueUpdate();
        }
    
        public void Dispose()
        {
            if (_timer != null)
            {
                _timer.Elapsed -= OnTimerElapsed;
                _timer.Stop();
                _timer.Dispose();
                _timer = null;
            }
        }
    }
    
    在应用退出或不再需要树结构时,调用root.Dispose()。

2. 频繁创建AvaloniaList导致的内存堆积

每次Update都会创建新的AvaloniaList<TreeNode>,旧列表若被UI绑定引用未及时释放,会造成内存堆积。

  • 优化方案:
    复用现有列表,避免频繁创建实例:
    private void AppendItems()
    {
        _updateEnqueued = false;
        _visibleChildren ??= new AvaloniaList<TreeNode>();
        _visibleChildren.Clear();
        AppendItems(_visibleChildren, this);
    }
    

3. MqttDelivery 实例的不必要持有

Add方法中直接存储完整的MqttDelivery实例,若该对象包含未释放的大资源(如字节数组、流),会持续占用内存。

  • 优化方案:
    仅存储必要数据,而非整个对象:
    // 替换原Delivery字段,只保留业务所需信息
    public string Topic { get; private set; }
    public string? HeaderJson { get; private set; }
    public string? PayloadJson { get; private set; }
    
    // 修改Add方法中的赋值逻辑
    if(level >= topic.Length)
    {
       Topic = delivery.Topic;
       HeaderJson = delivery.Header?.ToJsonString(true);
       PayloadJson = delivery.Payload?.ToJsonString(true);
       // 不再存储Delivery实例
       AddToHistory(delivery);
       return false;
    }
    

4. 历史消息的引用优化

ReceivedHistoryMessage若持有对MqttDelivery的引用,即使限制了数量,仍会占用额外内存。

  • 优化方案:
    历史消息仅存储序列化后的字符串或关键字段:
    public class ReceivedHistoryMessage
    {
        public string Topic { get; set; }
        public string? HeaderJson { get; set; }
        public string? PayloadJson { get; set; }
        public DateTime ReceivedTime { get; set; }
        // 移除对MqttDelivery的引用
    }
    
    // 修改AddToHistory方法
    public void AddToHistory(MqttDelivery mqttMessage)
    {
        while(UiHistoryMessages.Count >= 30)
            UiHistoryMessages.RemoveAt(UiHistoryMessages.Count - 1);
    
        UiHistoryMessages.Insert(0, new ReceivedHistoryMessage
        {
            Topic = mqttMessage.Topic,
            HeaderJson = mqttMessage.Header?.ToJsonString(true),
            PayloadJson = mqttMessage.Payload?.ToJsonString(true),
            ReceivedTime = DateTime.Now
        });
    }
    

5. Dispatcher.Post 的弱引用优化

EnqueueUpdate中使用Dispatcher.UIThread.Post传递Update方法,若队列中存在未执行的任务,会持有对TreeNode的强引用。

  • 优化方案:
    使用弱引用包装调用逻辑:
    public void EnqueueUpdate()
    {
       if(_updateEnqueued)
          return;
    
       _updateEnqueued = true;
       WeakReference<TreeNode> weakRef = new WeakReference<TreeNode>(this);
       Dispatcher.UIThread.Post(() =>
       {
           if (weakRef.TryGetTarget(out var node))
           {
               node.Update();
           }
       }, DispatcherPriority.Background);
    }
    

6. 工具验证

使用Visual Studio内存诊断工具或dotMemory生成内存快照,确认:

  • 占用内存最多的对象类型
  • 对象的引用链,定位未被释放的根源
  • 是否存在静态字段持有树节点的引用

内容的提问来源于stack exchange,提问作者Anthony

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 06:45:02