如何通过重要的WPF控件以确定的方式使用一个空闲内存?

时间:2012-01-24 14:02:39

标签: c# wpf memory-leaks garbage-collection controls

当使用来自UI的输入事件销毁和重建控件分配大量内存时,我遇到了问题。

显然,将Window的Content设置为null不足以释放所包含的Control正在使用的内存。它最终会被GCed。但是随着用于销毁和构造控件的输入事件变得更频繁,GC似乎跳过了一些对象。

我把它分解为这个例子:

XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseMove">
</Window>

C#:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private class HeavyObject : UserControl
        {
            private byte[] x = new byte[750000000];

            public HeavyObject()
            {
                for (int i = 0; i++ < 99999999; ) { } // just so we can follow visually in Process Explorer
                Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove
            }
        }

        public MainWindow()
        {
            InitializeComponent();

            //Works 1:
            //while (true)
            //{
            //    window.Content = null;
            //    window.Content = new HeavyObject();
            //}
        }

        private void window_MouseMove(object sender, MouseEventArgs e)
        {
            if (window.Content == null)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                // Works 2:
                //new HeavyObject();
                //window.Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove
                //return;
                window.Content = new HeavyObject();
            }
            else
            {
                window.Content = null;
            }
        }
    }
}

这会在每个MouseMove事件中分配一个~750MB对象(HeavyObject),并将其作为主窗口的内容。如果Cursor在窗口中保持静止,则内容更改将无休止地重新触发事件。 在下一次构建之前,对HeavyObject的引用是无效的,并且触发了对其遗骸的无效尝试。

我们在ProcessExplorer / Taskman中可以看到,有时会分配1.5GB或更多。如果没有,请疯狂移动鼠标并离开并重新进入窗口。如果你在没有LARGEADDRESSAWARE的情况下编译x86,你会得到一个OutOfMemoryException(限制~1.3GB)。

如果您在不使用鼠标输入的情况下在无限循环中分配HeavyObject(取消注释“Works 1”部分)或者通过鼠标输入触发分配但不将对象放入视觉中,则不会遇到此行为树(取消注释seciton“Works 2”)。

所以我认为这与视觉树懒惰地释放资源有关。但是还有另一个奇怪的效果:如果光标在窗口之外消耗了1.5GB,那么在MouseMove再次被触发之前,GC似乎不会启动。至少,只要没有触发其他事件,内存消耗似乎就会稳定下来。所以要么在某个地方留下长寿命的参考,要么在没有活动时GC变得懒惰。

对我来说似乎很奇怪。你能弄明白发生了什么吗?

修改 正如BalamBalam评论的那样:在销毁Control之前,可以取消引用Control后面的数据。那可能是B计划。不过可能有一个更通用的解决方案。

说这样的控件是坏代码是没有用的。我想知道为什么我没有引用的对象获得GCed如果我在解除引用它之后将UI单独留下几个Ticks,但是如果另一个(必须由用户输入创建)立即获取它,则永远存在的地方。

1 个答案:

答案 0 :(得分:7)

好的,我想我可能知道这里发生了什么。拿一小撮盐虽然......

我稍微修改了你的代码。

<强>的Xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" x:Name="window" MouseDown="window_MouseDown">
</Window>

代码背后

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private class HeavyObject : UserControl
        {
            private byte[] x = new byte[750000000];

            public HeavyObject()
            {
                for (int i = 0; i < 750000000; i++ )
                {                    
                    unchecked
                    {
                        x[i] = (byte)i;       
                    }
                }

                // Release optimisation will not compile the array
                // unless you initialize and use it. 
                Content = "Loadss a memory!! " + x[1] + x[1000]; 
            }
        }

        const string msg = " ... nulled the HeavyObject ...";

        public MainWindow()
        {
            InitializeComponent();
            window.Content = msg;
        }

        private void window_MouseDown(object sender, MouseEventArgs e)
        {
            if (window.Content.Equals(msg))
            {
                window.Content = new HeavyObject();
            }
            else
            {
                window.Content = msg;
            }
        }
    }
}

现在运行它并单击窗口。我将代码修改为不使用GC.Collect()并将设置内存设置为mousedown 。我正在发布模式进行编译并运行而不附加调试器

在窗口上慢慢点击

第一次点击,你看到内存使用量增加了750MBytes。只要您慢慢点击,后续点击就会在.. Nulled the heavy object..Loads a memory! 之间切换消息。分配和取消分配内存时会有轻微的延迟。内存使用量不应超过750MBytes,但是当重对象被清空时它也不会减少。这是设计为GC Large Object Heap is lazy and collects large object memory when new large object memory is needed。来自MSDN:

  

如果我没有足够的可用空间来容纳大对象分配请求,我将首先尝试从操作系统中获取更多段。如果失败了,那么我将触发第2代垃圾收集,希望释放一些空间。

点击窗口上的快速

大约20次点击。怎么了?在我的PC上,内存使用量没有增加,但主窗口现在不再更新消息了。它冻结了。我没有内存不足异常,我的系统的整体内存使用率保持不变。

使用Window上的MouseMove强调系统

现在通过更改此Xaml替换MouseDown以获取MouseMove事件(根据您的问题代码):

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseDown">
</Window>

我可以将鼠标移动一段时间,但最终会抛出一个OutofMemoryException 。强制异常的一个好方法是多次移动然后尝试最大化窗口。

理论

好的,这就是我的理论。由于您的UI线程在消息循环事件(MouseMove)内创建和删除内存时运行平稳,因此垃圾收集器无法跟上。大对象堆上的垃圾收集已完成when memory is needed。这也是一项昂贵的任务,因此尽可能不频繁地执行。

当在MouseMove中执行时,我们在快速单击开/关/开/关时注意到的滞后变得更加明显。如果要分配另一个大对象,根据该MSDN文章,GC将尝试收集(如果内存不可用)或将开始使用磁盘。

  

系统处于低内存状态:当我从操作系统收到高内存通知时会发生这种情况。如果我认为做第2代GC会很有效率,我会触发一个。

现在这部分很有趣,我猜这里但是

  

最后,截至目前,LOH并未作为收集的一部分进行压缩,但这是一个不应该依赖的实现细节。因此,为了确保GC不会移动某些东西,请始终固定它。现在拿走你新发现的LOH知识并控制堆。

因此,如果大对象堆未被压缩,并且您在紧密循环中请求多个大对象,是否可能导致碎片最终导致OutOfMemoryException,即使理论上应该有足够的内存? /强>

解决方案

让我们释放一下UI线程让GC室呼吸。将分配代码包装在异步Dispatcher调用中并使用低优先级,例如SystemIdle。这样,当UI线程空闲时,它将分配内存。

    private void window_MouseDown(object sender, MouseEventArgs e)
    {
        Dispatcher.BeginInvoke((ThreadStart)delegate()
            {
                if (window.Content.Equals(msg))
                {
                    window.Content = new HeavyObject();
                }
                else
                {
                    window.Content = msg;
                }
            }, DispatcherPriority.ApplicationIdle);
    }

使用这个我可以在表格上跳过鼠标指针,它永远不会抛出OutOfMemoryException 。无论是真正的工作还是“修复”了我不知道的问题,但它值得测试。

总结

  • 请注意,此大小的对象将在大对象堆上分配
  • 如果空间不足,LOH分配会触发GC
  • 在GC循环期间LOH没有被压缩,因此可能发生碎片,导致OutOfMemoryException,尽管理论上存在足够的内存
  • 理想情况下,重复使用大型对象,不要重新分配。如果那是不可能的:
    • 在后台线程中对您的大对象进行分组,以便与UI线程分离
    • 要允许GC室呼吸,请使用Dispatcher解除分配,直到应用程序空闲为止