I long time ago, I asked about Why doesn’t WINWORD.EXE quit after Closing the document from Delphi?.
It turns out that question is a lot harder in .NET than it is in Delphi. I already had a gut feeling of this when at clients I saw many more .NET applications leaking WINWORD.EXE stray processes than Delphi applications, even though both kinds were calling Quit on the Word application object.
Delphi has a deterministic way of coping with interfaces (hence you can do a One-liner RAII in Delphi, or make a memento): Interface references are released at the end of their scope.
.NET has non-deterministic finalization of the Common Language Runtime (CLR) and has Runtime Callable Wrappers (RCWs) around your COM references which are sometimes created “on the fly”.
The combination of non-deterministic finalization and RCWs can be very confusing, so lets start with the parts first.
Non-deterministic finalization
One of the biggest traps in C# (see Ten Traps in C# for C++ Programmers) is non-deterministic finalization is. It is explained very well by Non-Deterministic Destructors – What Are They?
There are quite a few non-deterministic parts in the whole finalization proces.
- It starts the point at which an object becomes eligible for finalization: you don’t know exactly when that will happen. The deterministic part is that an object has no root references. But the point in time that is determined is unknown. That point in time depends on when the garbage collector runs. You don’t know when the garbage collector starts to run: the garbage collector runs you. As the garbage collector runs in a separate thread, there are the basic multi-threading uncertainties. In addition, the garbage collecting thread however does not run continuously: so another uncertainty is when it will start to run. It will start when memory pressure gets high enough. But there are other factors like LatencyMode that the CLR takes into consideration. See the “further reading” for a few links on this.
- By now you should understand that garbage collection and final disposal are separated. When an object is eligible for finalization, the second non-deterministic part kicks in: during the garbage collection phase, all objects that are not reachable any more will have their finalizer called at some point in time. This is done in yet another thread: the finalization thread which finalizes objects from the finalization queue (which you should think of as a list, not a queue). In addition, you do not know in which order they are called. Since the execution order of fianlizers is unknown, any references between unreachable objects (private fields from one object to another object) are a no-go area in your finalizer code.
Mike Rosenblum wrote a great answer about finalizers on .net – Release Excel Object In My Destructor?
It is not wise to call the Garbage Collector yourself, as it manages 3 generations and does a balancing act to keep objects that are frequently used into the lowest two generations.
This is contrary to the popular belief of many posts. Just don’t follow that popular belief (:
One way to make this deterministic is to actively use the Dispose pattern. The COM support in .NET uses those. When implementing them yourself, you usually don’t have to write finalizers, especially since the introduction of SafeHandle in .NET 2.0 (they and the COM support in .NET implement the finalizers for you).
RCWs: the COM references are embedded in Runtime Callable Wrappers
The .NET COM support wraps RCWs around COM references: they make the transition from the managed .NET world into the unmanaged COM world. RCWs are automatically created when you access the COM objects described in an imported type library. When the garbage collector determines the RCW is not referenced any more, the RCW finalizer is called, decrementing the COM reference count. When all COM references to an object have been released, the COM object is removed from memory.
The trick is in this little piece of the Runtime Callable Wrapper documentation:
Each RCW maintains a cache of interface pointers on the COM object it wraps and releases its reference on the COM object when the RCW is no longer needed. The runtime performs garbage collection on the RCW.
This is where RCW and non-deterministic finalization come together.
As long as the CLR has not garbage collected the RCW (remember this event is non-deterministic!), it keeps a COM reference. As long as the COM references to a COM object exist, that COM object is being kept alive. Which means that WINWORD.EXE can be kept alive even though you think you are not using it any more. Arthur has written a nice SO answer about when COM references get incremented: Every time the COM object is passed from COM environment to .NET.
Combining RCW and memory management: make it as deterministic as possible
To make the management more deterministic you have to do these things:
- Make all your RCW references explicit.
In other words: Never use 2 dots with COM objects in an answer by VVS on c# – How to properly clean up Excel interop objects?.
Same with Save references retreived through COM indexes by Chris McGrath. - When you are absolutely sure you are done with an RCW reference, explicitly tell the CLR you are done with it.
Use the System.Runtime.InteropServices.Marshal.FinalReleaseComObject method for that (or repeated System.Runtime.InteropServices.Marshal.ReleaseComObject and mark how many references you forgot).
Or use the more fancy ReleaseWrapper by Marc Gravell in his answer to c# – Releasing temporary COM objects. - Watch for exceptions: introduce try…finally bocks to ensure you can release your RCW referenced in time.
- Avoid calling GC.Collect() and GC.WaitForPendingFinalizers().
In addition, read the Avoid the PIA events by Amit Mittal.
Finally something I need to research further: NetOffice – MS Office in .NET was mentioned by BTownTKD. It is supposed to be like PIA, but better, and more deterministic. And it is maintained.
References for further reading
COM inteoperability:
- .NET – COM Interoperability – CodeProject.
- COM Interoperability in .NET Framework: Part I – CodeProject.
- COM Wrappers.
- COM Callable Wrapper.
- Runtime Callable Wrapper.
- RCW Reference Counting Rules != COM Reference Counting Rules | Johannes Passing’s Blog.
- Runtime Callable Wrapper Internals and common pitfalls » Advanced .NET Debugging.
- Marshal.ReleaseComObject and CPU Spinning » Advanced .NET Debugging.
- Automating Office Programs with VB.Net / COM Interop – Xtreme Visual Basic Talk.
- .net – Release Excel Object In My Destructor – Stack Overflow.
- c# – How to properly clean up Excel interop objects – Stack Overflow.
- Avoid the PIA events by Amit Mittal.
- c# – Releasing temporary COM objects – Stack Overflow.
- c# – How close Winword.exe in my application – Stack Overflow.
- c# MSOffice Interop Word will not kill winword.exe – Stack Overflow.
Garbage collection and memory management:
- IDisposable: What Your Mother Never Told You About Resource Deallocation – CodeProject.
- Non-Deterministic Destructors – What Are They? – CodeProject.
- Ten Traps in C# for C++ Programmers – WindowsDevCenter.com.
- Entropy Overload: Commonly Confused Tidbits re .NET Garbage Collector.
- Non-Deterministic Destructors – What Are They? – CodeProject.
- 4 Essential Tips for High-Performance Garbage Collection on Servers | Philosophical Geek.
- #WINNING with C#: Garbage Collector .NET 4.
- Improving .NET Application Performance Part 4: Garbage Collection | Uptime & Performance Tips.
- CLR Inside Out: Large Object Heap Uncovered.
- Garbage Collection – Pt 3: Generations – Michel Triana (for completeness: Pt 2, Pt 1 and The Truth About Value Types – Fabulous Adventures In Coding – Site Home – MSDN Blogs.).
- Fundamentals of Garbage Collection.
- .NET Finalizer Memory Leak: Debugging with sos.dll in Visual Studio – If broken it is, fix it you should – Site Home – MSDN Blogs.
- Garbage Collector Basics and Performance Hints.
- CLR 4.0: Garbage Collection Changes.
- debugging – When does the .net garbage collector run? – Stack Overflow.
- Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework.
- c# – Can anyone explain this finalisation behaviour – Stack Overflow.
- .net – What are the Finalizer Queue and Control+ThreadMethodEntry? – Stack Overflow.
- .net – Safehandle in C# – Stack Overflow.
- SafeHandle Class (System.Runtime.InteropServices).
- c# – Calling null on a class vs Dispose() – Stack Overflow.
–jeroen
via: Why doesn’t WINWORD.EXE quit after Closing the document from Delphi? – Stack Overflow.
Filed under: .NET, Delphi, Development, Software Development