当使用来自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,但是如果另一个(必须由用户输入创建)立即获取它,则永远存在的地方。
答案 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 。无论是真正的工作还是“修复”了我不知道的问题,但它值得测试。
总结