die*_*nse 7 c# wpf controls garbage-collection memory-leaks
当使用来自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>
Run Code Online (Sandbox Code Playgroud)
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;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
这会在每个MouseMove事件中分配一个~750MB的对象(HeavyObject),并将其作为主窗口的内容.如果Cursor在窗口中保持静止,则内容更改将无休止地重新触发事件.在下一次构建之前,对重量对象的引用是无效的,并且触发了对其遗骸的无效尝试.
我们在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,但是如果另一个(必须由用户输入创建)立即获取它,则永远存在地点.
好吧,我想我可能知道这里发生了什么.拿一小撮盐虽然......
我稍微修改了你的代码.
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>
Run Code Online (Sandbox Code Playgroud)
代码背后
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;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
现在运行它并单击窗口.我修改了代码以不使用GC.Collect()并在mousedown上设置内存.我正在发布模式下编译并在没有附加调试器的情况下运行.
在窗口上慢慢单击.
第一次点击,你看到的内存使用量增加了750MBytes.随后单击可在两者之间切换消息.. Nulled the heavy object..
,Loads a memory!
只要您慢慢单击即可.分配和释放内存时会有轻微的延迟.内存使用量不应超过750MBytes,但是当重对象被清空时它也不会减少.这是设计的,因为GC大对象堆是惰性的,并且在需要新的大对象内存时收集大对象内存.来自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>
Run Code Online (Sandbox Code Playgroud)
我可以将鼠标移动它很长一段时间但最终它会抛出一个OutofMemoryException.强制异常的一个好方法是多次移动然后尝试最大化窗口.
理论
好的,这是我的理论.由于您的UI线程在消息循环事件(MouseMove)内创建和删除内存时运行平稳,因此垃圾收集器无法跟上.大对象堆上的垃圾收集在需要内存时完成.这也是一项昂贵的任务,因此尽可能不频繁地执行.
当在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);
}
Run Code Online (Sandbox Code Playgroud)
使用它我可以在整个表单上跳过鼠标指针,它永远不会抛出OutOfMemoryException.无论是真正的工作还是"修复"了我不知道的问题,但它值得测试.
结论