由于之前的建议比较通用,我认为发布我自己的解决这个异常的具体代码示例和我实现的背景更有用。首先是TL;DR版本:我使用了一个内部C++(未托管)编写的dll。我从我的.NET可执行文件中传入了一个特定大小的数组。未托管代码尝试写入由托管代码未分配的数组位置。这导致内存损坏,后来被设置为垃圾回收。当垃圾收集器准备收集内存时,它首先检查内存(和边界)的状态。当它发现损坏时,就会出现问题。
现在是详细版:
我正在使用内部开发的未托管C++ dll。我的GUI开发是使用C# .Net 4.0完成的。我调用了各种未托管方法。该dll有效地充当我的数据源。以下是来自dll的示例extern定义:
[DllImport(@"C:\Program Files\MyCompany\dataSource.dll",
EntryPoint = "get_sel_list",
CallingConvention = CallingConvention.Winapi)]
private static extern int ExternGetSelectionList(
uint parameterNumber,
uint[] list,
uint[] limits,
ref int size);
我将这些方法封装在我的接口中,以便在整个项目中使用:
public int GetSelectionList(uint parameterNumber,
ref uint[] messageList,
ref uint[] valueLimits,
int size)
{
int returnValue = -1;
returnValue = ExternGetSelectionList(parameterNumber,
messageList,
valueLimits,
ref size);
return returnValue;
}
这个方法的例子调用如下:
uint[] messageList = new uint[3];
uint[] valueLimits = new uint[3];
int dataReferenceParameter = 1;
MainNavigationWindow.MainNavigationProperty.DataSourceWrapper.GetSelectionList(
dataReferenceParameter,
ref messageList,
ref valueLimits,
BUFFERSIZE);
在GUI中,用户可以浏览包含各种图形和用户输入的不同页面。以前的方法使我能够获取数据以填充ComboBoxes。以下是在出现异常之前我的导航设置和调用示例:
在我的主窗口中,我设置了一个属性:
internal UserInterfacePage UserInterfacePageProperty
{
get
{
if (this.userInterfacePage == null)
{
this.userInterfacePage = new UserInterfacePage();
}
return this.userInterfacePage;
}
set { this.userInterfacePage = value; }
}
然后,当需要时,我导航到该页面:
MainNavigationWindow.MainNavigationProperty.Navigate(
MainNavigation.MainNavigationProperty.UserInterfacePageProperty)
虽然一切都运行良好,但我确实遇到了一些严重的问题。当使用对象 (NavigationService.Navigate Method (Object)) 进行导航时,默认情况下 IsKeepAlive
属性的设置为 true
。但问题比这更阴险。即使你在该页面的构造函数中将 IsKeepAlive
值明确设置为 false
,垃圾回收器仍会像它是 true
一样不管它。对于我的许多页面来说,这并不是什么大问题。它们的内存占用很小,没有太多的操作。但其中许多页面上有一些大型高度详细的图形,用于说明目的。不久之后,我们设备的操作员正常使用此界面就会导致巨大的内存分配,这些内存永远不会清除,最终会堵塞机器上的所有进程。在最初的开发热潮从海啸到更像是潮汐波之后,我终于决定彻底解决内存泄漏问题。我不会详细介绍我实施的所有技巧来清理内存 (WeakReference 图像,解除 Unload() 上的事件处理程序,使用实现 IWeakEventListener 接口的自定义计时器等...)。我所做的关键更改是使用 Uri 而不是对象进行页面导航 (NavigationService.Navigate Method (Uri))。使用此类型的导航有两个重要的区别:
IsKeepAlive
默认设置为 false
。
- 垃圾回收器现在会尝试清理导航对象,就好像
IsKeepAlive
被设置为 false
一样。
所以现在我的导航看起来像:
MainNavigation.MainNavigationProperty.Navigate(
new Uri("/Pages/UserInterfacePage.xaml", UriKind.Relative));
还有一点需要注意:这不仅影响垃圾收集器对对象的清理方式,而且影响它们在内存中的初始分配,我很快就会发现。
一切似乎都很顺利。当我浏览图形密集页面时,我的内存会迅速清理到接近最初状态,直到我遇到了具有特定调用数据源dll填充某些comboBoxes的特定页面。然后我就遇到了这个讨厌的FatalEngineExecutionError
。经过数日的研究和找到模糊的建议或高度特定的解决方案(这些解决方案并不适用于我),以及释放我个人编程武器库中的所有调试工具,我最终决定唯一确保解决这个问题的方法是采取极端措施,逐个复制此特定页面的每个元素、方法和行,直到我最终找到引发此异常的代码。这就像我所暗示的那样单调而痛苦,但我最终找到了问题所在。
实际上是未托管的dll分配内存以写入数据到我提供的数组中的方式出了问题。该特定方法实际上会查看参数编号,并根据期望写入我发送的数组的数据量来分配特定大小的数组。导致代码崩溃的部分:
uint[] messageList = new uint[2];
uint[] valueLimits = new uint[2];
int dataReferenceParameter = 1;
MainNavigationWindow.MainNavigationProperty.DataSourceWrapper.GetSelectionList(
dataReferenceParameter,
ref messageList,
ref valueLimits,
BUFFERSIZE);
这段代码看起来和上面的示例完全一样,但有一个微小的区别。我分配的数组大小是
2而不是
3。我这样做是因为我知道这个特定的ComboBox只有两个选择项,而页面上的其他ComboBox都有三个选择项。然而,非托管代码并没有像我看到的那样看待它。它得到了我交给它的数组,并试图将一个大小为
3 的数组写入我的大小为
2 的分配内存中,就这样。 *
砰!* *
崩溃!* 我将分配大小更改为3,错误消失了。
现在,这段特定的代码已经运行了至少一年,没有出现这个错误。但是通过
Uri
而不是
Object
导航到这个页面引起了崩溃。这意味着由于我使用的导航方法不同,初始对象必须以不同的方式分配。因为使用我的旧导航方法,内存只是堆积在一起,永远不会被清理,所以似乎即使在一个或两个小位置上有点损坏也无关紧要。一旦垃圾收集器必须对该内存进行实际操作(如清理它),它就会检测到内存损坏并抛出异常。具有讽刺意味的是,
我的主要内存泄漏掩盖了致命的内存错误!
显然,我们将审查此接口,以避免未来发生这种简单假设导致的崩溃。希望这可以帮助一些其他人找出他们自己代码中的问题。