释放COM对象的正确方法是什么?

20
有时当我关闭应用程序并尝试释放一些COM对象时,调试器会发出警告:

检测到 RaceOnRCWCleanUp

如果我编写一个使用COM对象的类,是否需要实现IDisposable接口,并在IDisposable.Dispose中调用Marshal.FinalReleaseComObject以正确释放它们?
如果没有手动调用Dispose,那么是否仍然需要在终结器中释放它们,或者GC会自动释放它们? 现在我在终结器中 调用Dispose(false),但我想知道这样做是否正确。
我使用的COM对象还有一个事件处理程序,该类侦听它。 显然,事件会在另一个线程上引发,那么如果在处理类时触发了该事件,我应该如何正确处理它?

3
这是提醒手动内存管理并不是一个好主意。 MDA警告可能会导致COM服务器的严重崩溃。 在应用程序关闭时尝试处理任何内容都是无意义的,它将在一毫秒后被终结。垃圾回收器已经知道如何处理这些,避免干扰。 - Hans Passant
3个回答

19
首先,当进行Excel互操作时,您永远不需要调用Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)。这是一种令人困惑的反模式,但任何关于此的信息(包括来自Microsoft的信息)都表明您必须从.NET手动释放COM引用是不正确的。事实是,.NET运行时和垃圾回收器正确跟踪和清理COM引用。
其次,如果您想确保在进程结束时(以便Excel进程将关闭),清除对进程外COM对象的COM引用,您需要确保垃圾回收器运行。通过调用GC.Collect()GC.WaitForPendingFinalizers()来正确执行此操作。调用两次是安全的,并确保循环也被清理。
第三,在调试器下运行时,本地引用将被人为地保持活动状态,直到方法结束(以便本地变量检查工作)。因此,从同一方法中清除像rng.Cells这样的对象的GC.Collect()调用是无效的。您应该将执行COM互操作的代码与GC清理分开成单独的方法。
通常的模式如下:
Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now Let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

关于这个问题,有很多虚假信息和混淆,包括许多在MSDN和StackOverflow上的帖子。

最终使我决定仔细研究并找出正确建议的是这篇文章https://blogs.msdn.microsoft.com/visualstudio/2010/03/01/marshal-releasecomobject-considered-dangerous/,以及在某些StackOverflow答案中发现了在调试器下保持引用活动的问题。


'WrapperThatCleansUp' 做了两件事情:(1)调用执行 COM 调用的 Sub,(2)调用垃圾回收器进行清理。更高层次的代码将调用 'WrapperThatCleansUp'。就像这个例子一样,您应该将 COM 工作放在一个单独的程序中,与您调用 GC 的程序分开,以确保本地变量的清晰范围。 - Govert
这真是太棒了 - 最好的解决方案!我最近参加了微软的课程,他们在这个问题上所教和推荐的方法完全不同,而且代码更长。他们推荐的是:1)创建一个实现IDisposable接口的类,并编写COM对象内部的方法(用于创建文档、保存等)。2)从任何你想要的地方调用该类,使用Using语句 - 这样就可以处理对象被释放的问题...但是,你的代码是我见过的最好的,也是最短的,非常感谢! - LuckyLuke82
1
是的,类级别的引用将保持Excel处于活动状态,除非您在调用GC之前明确将其设置为“Nothing”。 - Govert
1
@Murphybro2,这两种说法都是不正确的。垃圾回收并不会停止所有进程,它只会停止当前进程中的线程。当运行完整个垃圾回收时,才会重置第二代。 (例如,请参见此处:https://learn.microsoft.com/zh-cn/dotnet/standard/garbage-collection/fundamentals)。最后(!)的想法不是为了“ Hammer”GC,而是要确保在与Excel交互完成的COM工作之后至少运行一次。这将确保清理COM对象而无需显式的COM引用管理。 - Govert
1
@Govert 尽管这种解决方案对于短期的Interop来说更容易,但我不确定在VSTO设置中采用这种方法是否合适。如果您有非常长时间运行的会话,那么在整个会话期间,您不会不断积累COM对象吗?也就是说,会出现内存泄漏问题吧? - Alexander Høst
显示剩余11条评论

16

基于我使用不同的COM对象(进程内或进程外)的经验,我建议对于每一个COM/.NET边界交互,使用一次Marshal.ReleaseComObject(例如,如果您引用COM对象以检索另一个COM引用)。

由于我决定将COM互操作清理推迟到垃圾回收时期,因此我遇到了许多问题。同时请注意,我从未使用Marshal.FinalReleaseComObject - 某些COM对象是单例对象,这种方法无法很好地与这些对象配合使用。

在终极器中(或者是来自众所周知的IDisposable实现的Dispose(false))中进行托管对象的任何操作都是被禁止的。您不能依赖于最终器中任何.NET对象的引用。您可以释放IntPtr,但不能释放COM对象,因为它可能已经被清理掉了。


如果使用COM对象的类将存在直到应用程序终止,会发生什么情况?如果COM对象的“文档”要求调用某种Deactivate方法怎么办?(我不确定它的作用,但我尝试不调用它,没有发生任何不良反应。) - Alvin Wong

12

这里有一篇相关的文章:http://www.codeproject.com/Tips/235230/Proper-Way-of-Releasing-COM-Objects-in-NET

简而言之:

1) Declare & instantiate COM objects at the last moment possible.
2) ReleaseComObject(obj) for ALL objects, at the soonest moment possible.
3) Always ReleaseComObject in the opposite order of creation.
4) NEVER call GC.Collect() except when required for debugging.

只有在GC自然发生时,COM引用才会被完全释放。这就是为什么很多人需要使用FinalReleaseComObject()和GC.Collect()来强制销毁对象的原因。这两者都对于有问题的Interop代码是必需的。

Dispose不会被GC自动调用。当对象被处理时,析构函数会被调用(在不同的线程中)。这通常是您可以释放任何非托管内存或COM引用的地方。

析构函数:http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx

... 当您的应用程序封装了像窗口、文件和网络连接等非托管资源时,您应该使用析构函数来释放这些资源。当对象有资格被销毁时,垃圾收集器运行对象的Finalize方法。


为什么我们不应该在GC.WaitForPendingFinalizers()之后调用GC.Collect()? - Martin Braun
我自己也不是100%确定,但如果你阅读文章,它说“GC.Collect将停止操作系统中的所有.NET进程以收集垃圾。每次调用GC.Collect,您可能会将垃圾从第0代提升到第1代,然后再提升到第2代。一旦您的垃圾达到第2代,您必须终止进程以恢复内存。”因此,这可能是由于强制进行太多GC调用而导致COM对象在GC中被卡住时间过长的潜在风险。 - James Wilkins
频繁调用GC.Collect不仅不能及时释放内存,而且会导致长生命周期的对象(即第二代)被GC较少检查;问题在于每次GC跳过这些对象(即仍存在对该对象的引用),对象的代数会提升一次,因此通过调用GC.Collect会使对象过早地晋升,从而欺骗GC认为它们具有较长的生命周期,即使它们实际上并没有。 - jonvw
如果你说的话和我完全一样,那就没有必要提到可能需要终止应用程序的部分(在许多情况下,我发现这是正确的)。我希望你没有因为不同意我的评论而对答案进行了负评。如果你对答案本身有问题,请解释。如果不是你,那么现在正在阅读这篇文章的人应该这样做。 - James Wilkins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接