简单的 WPF 示例导致不受控制的内存增长

Posted

技术标签:

【中文标题】简单的 WPF 示例导致不受控制的内存增长【英文标题】:Simple WPF sample causes uncontrolled memory growth 【发布时间】:2010-09-16 13:54:46 【问题描述】:

我将我在一个应用程序中看到的一个问题归结为一个非常简单的复制示例。我需要知道是否有什么不对劲或我遗漏了什么。

不管怎样,下面是代码。行为是代码在内存中运行并稳定增长,直到它因 OutOfMemoryException 而崩溃。这需要一段时间,但行为是正在分配对象并且没有被垃圾收集。

我已经进行了内存转储并在一些事情上运行了 !gcroot 并使用 ANTS 来找出问题所在,但我已经研究了一段时间并且需要一些新的眼光。

此复制示例是一个简单的控制台应用程序,它创建一个 Canvas 并向其添加一个 Line。它不断地这样做。这就是代码所做的全部。它时不时地休眠,以确保 CPU 不会因为过度使用而导致系统无响应(并确保 GC 无法运行时不会出现异常情况)。

有人有什么想法吗?我仅在 .NET 3.0、.NET 3.5 和 .NET 3.5 SP1 上尝试过此操作,并且在所有三个环境中都发生了相同的行为。

另请注意,我也将此代码放入 WPF 应用程序项目中,并在单击按钮时触发代码,它也发生在那里。

使用系统; 使用 System.Collections.Generic; 使用 System.Linq; 使用 System.Text; 使用 System.Windows.Controls; 使用 System.Windows.Shapes; 使用 System.Windows; 命名空间 SimplestReproSample 课堂节目 [STA线程] 静态无效主要(字符串 [] 参数) 长计数 = 0; 而(真) 如果(计数++ % 100 == 0) // 休眠一段时间以确保我们没有用完整个 CPU System.Threading.Thread.Sleep(50); 构建画布(); [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] 私有静态无效 BuildCanvas() 画布 c = 新画布(); 线线 = 新线(); 线.X1 = 1; 线.Y1 = 1; 线.X2 = 100; 线.Y2 = 100; line.Width = 100; c.Children.Add(line); c.Measure(新尺寸(300, 300)); c.Arrange(新矩形(0, 0, 300, 300));

注意:下面的第一个答案有点离题,因为我已经明确指出,在 WPF 应用程序的按钮单击事件期间会发生同样的行为。但是,我没有明确说明在那个应用程序中我只进行了有限次数的迭代(比如 1000 次)。这样做可以让 GC 在您单击应用程序时运行。另请注意,我明确表示我已经进行了内存转储,并发现我的对象是通过 !gcroot 植根的。我也不同意 GC 将无法运行。 GC 不在我的控制台应用程序的主线程上运行,特别是因为我在双核机器上,这意味着 Concurrent Workstation GC 处于活动状态。但是,消息泵,是的。

为了证明这一点,下面是一个在 DispatcherTimer 上运行测试的 WPF 应用程序版本。它在 100 毫秒的计时器间隔内执行 1000 次迭代。有足够的时间处理来自泵的任何消息并保持较低的 CPU 使用率。

使用系统; 使用 System.Collections.Generic; 使用 System.Linq; 使用 System.Text; 使用 System.Windows; 使用 System.Windows.Controls; 使用 System.Windows.Shapes; 命名空间 SimpleReproSampleWpfApp 公共部分类 Window1:窗口 私有 System.Windows.Threading.DispatcherTimer _timer; 公共窗口1() 初始化组件(); _timer = new System.Windows.Threading.DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(100); _timer.Tick += new EventHandler(_timer_Tick); _timer.Start(); [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] 无效运行测试() for (int i = 0; i

注意2:我使用了第一个答案中的代码,我的记忆增长非常缓慢。请注意,1ms 比我的示例慢得多,迭代次数也少得多。在开始注意到增长之前,您必须让它运行几分钟。 5 分钟后,它从 30MB 的起点变为 46MB。

注意 3:删除对 .Arrange 的调用完全消除了增长。不幸的是,该调用对我的使用非常重要,因为在许多情况下,我是从 Canvas(通过 RenderTargetBitmap 类)创建 PNG 文件。如果不调用 .Arrange,它根本不会布局画布。

【问题讨论】:

【参考方案1】:

我能够使用您提供的代码重现您的问题。内存不断增长,因为 Canvas 对象从未被释放;内存分析器表明 Dispatcher 的 ContextLayoutManager 正在保留它们(以便它可以在必要时调用 OnRenderSizeChanged)。

似乎一个简单的解决方法是添加

c.UpdateLayout()

BuildCanvas的末尾。

也就是说,请注意CanvasUIElement;它应该在UI中使用。它不是为用作任意绘图表面而设计的。正如其他评论者已经指出的那样,创建数千个 Canvas 对象可能表明存在设计缺陷。我意识到您的生产代码可能更复杂,但如果它只是在画布上绘制简单的形状,则基于 GDI+ 的代码(即 System.Drawing 类)可能更合适。

【讨论】:

关于为什么垃圾收集是一个糟糕的内存管理系统的课程到此结束,尤其是在声称易于使用的语言中。 我 100% 完全同意您的观点,您的回答似乎确实解决了问题。我在 2 年后才开始使用此代码,不幸的是,目前没有简单的方法可以更改它。 我已经看到我的 Canvas 通过一些 SizeChangeInfo 对象连接到 ContextLayoutManager,但是我在 WPF 类上太绿了,无法弄清楚(显然)。谢谢。 难以置信!但它帮助我避免了 MediaElement 中的内存泄漏!【参考方案2】:

.NET 3 和 3.5 中的 WPF 存在内部内存泄漏。它仅在某些情况下触发。我们永远无法弄清楚究竟是什么触发了它,但我们在我们的应用程序中有它。显然它已在 .NET 4 中修复。

我觉得和this blog post中提到的一样

无论如何,将以下代码放在App.xaml.cs 构造函数中为我们解决了这个问题

public partial class App : Application

   public App() 
    
       new HwndSource(new HwndSourceParameters()); 
    

如果没有其他方法可以解决,试试看

【讨论】:

这当然很有趣,但这是在 Vista 上,并且此源博客文章表明它仅适用于 XP。 blogs.msdn.com/b/jgoldb/archive/2008/02/04/…【参考方案3】:

通常在 .NET 中,GC 会在超过某个阈值时触发对象分配,它不依赖于消息泵(我无法想象它与 WPF 不同)。

我怀疑 Canvas 对象以某种方式深深植根于内部或其他东西。如果在 BuildCanvas 方法完成之前执行 c.Children.Clear(),内存增长会显着减慢。

无论如何,正如这里的评论者所说,这样使用框架元素是非常不寻常的。为什么需要这么多画布?

【讨论】:

这是一个大型 WPF 应用程序,随着时间的推移内存增长非常缓慢。使用几天后,内存使用率非常高,并且在检查了内存转储后,我已经能够重新创建导致增长的场景。 这是一个绘图应用程序; file-new 和诸如此类的东西在很长一段时间内使内存不断增长,并且一直呈上升趋势。创建新绘制元素、制作新文档、新绘制元素等的行为会导致泄漏。此示例代码可以更快地完成用户看到的操作。 执行 .Children.Clear 无效。您实际上可以在根本不向画布添加任何子项的情况下看到行为。 我们有一个 WPF 应用程序,在一个场景中约 3000 个框架元素上具有超过 10k 的绑定,随着时间的推移没有任何过度增长等。但是,我们重用框架元素而不是删除/添加新元素. 有没有机会调用 .Arrange 方法?【参考方案4】:

编辑 2: 显然不是答案,而是这里的答案和 cmets 之间来回反复的一部分,所以我不会删除它。

GC 永远没有机会收集这些对象,因为您的循环及其阻塞调用永远不会结束,因此消息泵和事件永远不会轮到它们。如果您使用某种Timer 以便消息和事件实际上有机会处理,那么您可能无法耗尽所有内存。

编辑:只要间隔大于零,以下内容就不会占用我的内存。即使间隔只有 1 Tick,只要不为 0。如果为 0,则返回无限循环。

public partial class Window1 : Window 
    Class1 c;
    DispatcherTimer t;
    int count = 0;
    public Window1() 
        InitializeComponent();

        t = new DispatcherTimer();
        t.Interval = TimeSpan.FromMilliseconds( 1 );
        t.Tick += new EventHandler( t_Tick );
        t.Start();
    

    void t_Tick( object sender, EventArgs e ) 
        count++;
        BuildCanvas();
    

    private static void BuildCanvas() 
        Canvas c = new Canvas();

        Line line = new Line();
        line.X1 = 1;
        line.Y1 = 1;
        line.X2 = 100;
        line.Y2 = 100;
        line.Width = 100;
        c.Children.Add( line );

        c.Measure( new Size( 300, 300 ) );
        c.Arrange( new Rect( 0, 0, 300, 300 ) );
    

【讨论】:

更不用说不断重建 FrameworkElement 是非常昂贵的。 查看下一个答案的评论。 +1 也许不是 the 答案,但看起来像是一次有效的尝试。在 -1 看到它让我很伤心。

以上是关于简单的 WPF 示例导致不受控制的内存增长的主要内容,如果未能解决你的问题,请参考以下文章

其他元素折叠时 WPF UI 元素不会增长

C 语言内存四区原理 ( 栈内存属性增长方向 | 栈内存开口方向 | 代码示例 )

当master down掉后,pt-heartbeat不断重试会导致内存缓慢增长

Tomcat内存增长

WPF 使用frame加载page内存暴涨问题 坑

自增长字段值的连续递增实现