13

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

无论如何,下面是代码。行为是代码在内存中运行并稳定增长,直到它因 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 < 1000; i++)
            {
                构建画布();
            }
        }

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

        void _timer_Tick(对象发送者,EventArgs e)
        {
            _timer.Stop();

            运行测试();

            _timer.Start();
        }
    }
}

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

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

4

4 回答 4

10

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

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

c.UpdateLayout()

到结束BuildCanvas

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

于 2008-10-11T02:17:13.450 回答
2

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

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

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

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

如果没有其他方法可以解决它,请尝试并查看

于 2010-09-01T02:20:00.060 回答
1

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

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

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

于 2008-10-10T18:35:40.527 回答
0

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

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

编辑:只要间隔大于零,以下内容就不会占用我的内存。即使间隔只有 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 ) );
    }
}
于 2008-10-10T17:45:37.210 回答