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

我已经将我在其中一个应用程序中看到的一个问题归结为一个非常简单的复制样本。 我需要知道是否有什么不对劲或者我缺少的东西。

无论如何,下面是代码。 行为是代码在内存中运行并稳定增长,直到它与OutOfMemoryException崩溃。 这需要一段时间,但行为是对象被分配,而不是垃圾收集。

我已经采取内存转储并运行!gcroot对某些事情以及使用ANTS来弄清楚问题是什么,但我已经有一段时间了,需要一些新的眼睛。

此复制示例是一个简单的控制台应用程序,它创建一个Canvas并为其添加一行。 它不断地这样做。 这就是所有代码所做的。 它不时地睡觉以确保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
 {
    课程
     {
         [STAThread]
         static void Main(string [] args)
         {
             long count = 0;
            而(真)
             {
                 if(count ++%100 == 0)
                 {
                     //睡一会儿,以确保我们不会耗尽整个CPU
                     System.Threading.Thread.Sleep(50);
                 }
                 BuildCanvas();
             }
         }

         [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
         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(线);

             c.Measure(新尺寸(300,300));
             c.Arrange(new Rect(0,0,300,300));
         }
     }
 }

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

为了certificate这一点,这是一个在DispatcherTimer上运行测试的WPF应用程序版本。 它在100ms定时器间隔期间执行1000次迭代。 有足够的时间处理泵中的任何消息并保持低CPU使用率。

使用系统;
使用System.Collections.Generic;
使用System.Linq;
使用System.Text;
使用System.Windows;
使用System.Windows.Controls;
使用System.Windows.Shapes;

命名空间SimpleReproSampleWpfApp
 {
     public partial class Window1:Window
     {
         private System.Windows.Threading.DispatcherTimer _timer;

         public Window1()
         {
            的InitializeComponent();

             _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)]
         void RunTest()
         {
             for(int i = 0; i <1000; i ++)
             {
                 BuildCanvas();
             }
         }

         [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
         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(线);

             c.Measure(新尺寸(300,300));
             c.Arrange(new Rect(0,0,300,300));
         }

         void _timer_Tick(object sender,EventArgs e)
         {
             _timer.Stop();

            的runTest();

             _timer.Start();
         }
     }
 }

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

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

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

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

c.UpdateLayout() 

BuildCanvas的末尾。

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

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

我认为它与本博文中提到的相同

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

 public partial class App : Application { public App() { new HwndSource(new HwndSourceParameters()); } } 

如果没有别的办法解决它,那试试看吧

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

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

无论如何,作为这里提到的评论者,这种框架元素的使用是非常不寻常的。 你为什么需要这么多canvas?

编辑2:显然不是答案,但是这里的答案和评论是来回的一部分,所以我不会删除它。

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