在 Finalizer 中处理 MemoryCache 会引发 AccessViolationException

Posted

技术标签:

【中文标题】在 Finalizer 中处理 MemoryCache 会引发 AccessViolationException【英文标题】:Disposing MemoryCache in Finalizer throws AccessViolationException 【发布时间】:2014-12-03 13:31:41 【问题描述】:

编辑 有关更多详细信息,请参阅问题底部的编辑说明。

原始问题

我有一个 CacheWrapper 类,它在内部创建并保留 .NET MemoryCache 类的实例。

MemoryCache 将自身挂接到 AppDomain 事件中,因此除非明确处置,否则它永远不会被垃圾回收。您可以使用以下代码验证这一点:

Func<bool, WeakReference> create = disposed => 
    var cache = new MemoryCache("my cache");
    if (disposed)  cache.Dispose(); 
    return new WeakReference(cache);
;

// with false, we loop forever. With true, we exit
var weakCache = create(false);
while (weakCache.IsAlive)

    "Still waiting...".Dump();
    Thread.Sleep(1000);
    GC.Collect();
    GC.WaitForPendingFinalizers();

"Cleaned up!".Dump();

由于这种行为,我认为我的 MemoryCache 实例应该被视为非托管资源。换句话说,我应该确保它在 CacheWrapper 的终结器中被释放(CacheWrapper 本身就是 Disposable 遵循标准的 Dispose(bool) 模式)。

但是,我发现当我的代码作为 ASP.NET 应用程序的一部分运行时,这会导致问题。卸载应用程序域时,终结器在我的 CacheWrapper 类上运行。这反过来又试图处置MemoryCache 实例。这是我遇到问题的地方。似乎Dispose 尝试从 IIS 加载一些配置信息,但失败了(可能是因为我正在卸载应用程序域,但我不确定。这是我的堆栈转储:

MANAGED_STACK: 
    SP               IP               Function
    000000298835E6D0 0000000000000001 System_Web!System.Web.Hosting.UnsafeIISMethods.MgdGetSiteNameFromId(IntPtr, UInt32, IntPtr ByRef, Int32 ByRef)+0x2
    000000298835E7B0 000007F7C56C7F2F System_Web!System.Web.Configuration.ProcessHostConfigUtils.GetSiteNameFromId(UInt32)+0x7f
    000000298835E810 000007F7C56DCB68 System_Web!System.Web.Configuration.ProcessHostMapPath.MapPathCaching(System.String, System.Web.VirtualPath)+0x2a8
    000000298835E8C0 000007F7C5B9FD52 System_Web!System.Web.Hosting.HostingEnvironment.MapPathActual(System.Web.VirtualPath, Boolean)+0x142
    000000298835E940 000007F7C5B9FABB System_Web!System.Web.CachedPathData.GetPhysicalPath(System.Web.VirtualPath)+0x2b
    000000298835E9A0 000007F7C5B99E9E System_Web!System.Web.CachedPathData.GetConfigPathData(System.String)+0x2ce
    000000298835EB00 000007F7C5B99E19 System_Web!System.Web.CachedPathData.GetConfigPathData(System.String)+0x249
    000000298835EC60 000007F7C5BB008D System_Web!System.Web.Configuration.HttpConfigurationSystem.GetApplicationSection(System.String)+0x1d
    000000298835EC90 000007F7C5BAFDD6 System_Configuration!System.Configuration.ConfigurationManager.GetSection(System.String)+0x56
    000000298835ECC0 000007F7C63A11AE System_Runtime_Caching!Unknown+0x3e
    000000298835ED20 000007F7C63A1115 System_Runtime_Caching!Unknown+0x75
    000000298835ED60 000007F7C639C3C5 System_Runtime_Caching!Unknown+0xe5
    000000298835EDD0 000007F7C7628D86 System_Runtime_Caching!Unknown+0x86
    // my code here

是否有任何已知的解决方案?我认为我确实需要在终结器中处理 MemoryCache 是否正确?

编辑

This article 验证了 Dan Bryant 的回答并讨论了许多有趣的细节。特别是,他涵盖了StreamWriter 的情况,它面临着与我的情况类似的情况,因为它想在处理时刷新它的缓冲区。这篇文章是这样说的:

一般来说,终结器可能无法访问托管对象。 然而,对关闭逻辑的支持是必要的 相当复杂的软件。 Windows.Forms 命名空间处理这个 使用 Application.Exit,它会启动有序关闭。什么时候 设计库组件,有一种方法是有帮助的 支持与现有集成的关闭逻辑 逻辑上相似的 IDisposable(这避免了必须定义一个 没有任何内置语言支持的 IShutdownable 接口)。这 通常是通过支持有序关机来完成的 IDisposable.Dispose 被调用,并且在它被调用时中止关闭 不是。如果终结器可以用来做一个更好的 尽可能有序关机。

微软也遇到了这个问题。 StreamWriter 类 拥有一个 Stream 对象; StreamWriter.Close 将刷新其缓冲区并 然后调用 Stream.Close。但是,如果 StreamWriter 没有关闭,它的 终结器无法刷新其缓冲区。微软“解决”了这个问题 没有给 StreamWriter 一个终结器,希望程序员会 注意丢失的数据并推断它们的错误。这是一个完美的 需要关闭逻辑的示例。

话虽如此,我认为应该可以使用 Wea​​kReference 实现“托管终结”。基本上,让你的类在创建对象时注册一个对其自身的 WeakReference 和一个带有一些队列的 finalize 操作。然后,队列由后台线程或计时器监控,当它配对的 WeakReference 被收集时,它会调用适当的操作。当然,您必须小心,您的 finalize 操作不会无意中保留类本身,从而完全阻止收集!

【问题讨论】:

我很确定你永远不应该在终结器中处理托管对象,而应该在正常的IDisposable.Dispose 中完成。终结器应该只清理非托管对象。 @juharr:不过,在这种情况下,对象是“非托管”的,除非明确处置,否则它永远不会被垃圾回收。 @juharr:托管资源是一个持有外部资源的对象,但如果它被放弃就会释放它们。持有外部资源但在放弃时不会释放它们的事物需要被视为非托管资源,即使在许多情况下它们无法用终结器清理,因此唯一的补救措施是确保他们从一开始就永远不会被遗弃。 【参考方案1】:

您不能在终结器中 Dispose 托管对象,因为它们可能已经被终结(或者,正如您在此处看到的,部分环境可能不再处于您期望的状态。)这意味着如果您包含必须显式处置的类,则您的类也必须显式处置。没有办法“作弊”并使 Disposal 自动进行。不幸的是,在这种情况下,垃圾收集是一种泄漏抽象。

【讨论】:

有趣。此规则是否有任何明确的文档?我读过的东西总是使用术语“非托管资源”,我认为它的意思是任何不能被 GC 清理的东西。这是否也意味着您基本上只能在终结器中使用操作系统句柄?例如,如果我有一个要删除的文件的字符串路径,那么我的终结器引用该字符串以删除该文件是否有效? 你真的不应该在你的终结器中运行任何没有使用像IntPtr这样的句柄显式关闭非托管资源的代码。在终结器线程上执行任何类型的 I/O 都是一个非常糟糕的主意,并且可能会导致您所看到的各种问题。您不知道终结器运行时的安全上下文是什么。 @MikeStrobel 感谢您提供更多详细信息。你能指出任何官方文档吗? @ChaseMedallion MSDN documentation for Object.Finalize 中有很多有用的信息。一些建议,例如不执行 I/O,可能需要在字里行间进行解读:您不知道什么时候会发生终结,或者在哪个线程上,这会自动对任何阻塞和/或有安全要求的操作产生影响。跨度> 据我所知,MS 从未真正定义过“非托管资源”。它提供了示例,但没有定义。我会按照您的定义来定义:如果对象被遗弃,则不会自动清理的外部资源。【参考方案2】:

我建议带有终结器的对象通常不应该暴露给外部世界,并且应该只持有对最终确定实际需要的事物的强引用,并且不会暴露给外部世界不期望它们的任何事物用于该目的。面向公众的类型本身不应具有终结器,而应将清理逻辑封装在可终结类的私有实例中,其目的是封装此类逻辑。

只有当另一个对象设计与终结器接口时,终结器才真正有意义地尝试清理另一个对象拥有的资源。我想不出任何地方框架类在适当的钩子中进行了设计,但会提供一个示例,微软可以如何设计它们来做到这一点。

File 对象可以提供带有线程安全订阅和取消订阅方法的事件,当File 对象接收到Dispose 调用或finalize 请求时,这些方法将触发(首先通知最后一个订阅者)。该事件将在调用Finalize 和封装文件实际关闭之间触发,并且可以被外部缓冲类用作它需要向File 提供它收到的任何信息的信号但尚未通过。

请注意,为了使这样的事情正常和安全地工作,File 对象中具有终结器的部分必须不向公众公开,并且它使用长弱引用来确保如果它在面向公众的对象仍然存在时运行,它将重新注册自己以完成最终确定。请注意,如果对 WeakReference 对象的唯一引用存储在可终结对象中,则如果可终结对象符合终结条件,则其 Target 属性可能会失效即使引用的实际目标仍然存在 /em>。有缺陷的设计,恕我直言,必须小心处理。

可以使用可以协作的终结器来设计对象(最简单的方法通常是在组中只有一个对象使用终结器),但如果事情不是为了与终结器协作而设计的,那么最好的办法通常是让终结器发出警报,指示“这个对象不应该是Disposed,但不是;因为它不是,资源会泄漏,没有什么可做的关于它,除了修复代码以在将来正确处理对象”。

【讨论】:

以上是关于在 Finalizer 中处理 MemoryCache 会引发 AccessViolationException的主要内容,如果未能解决你的问题,请参考以下文章

一文了解OOM及解决方案

jdk源码解析--Finalizer类

JVM发生OOM的 8 种原因及解决办法

Java面试宝典!java可以做ios开发吗

IDEA太强悍了!java软件开发工程师证书考试费用

Finalizer 导致的OOM