none
How to notify CLR to reclaim the memory for objects created from COM side call RRS feed

  • Question

  • I have a COM visible CSharp class MyInterface. This class can take string instructions: "Run", "Terminate". When the class gets instruction "Run", it creates a bunch of .net objects before running the business code. When it gets instruction, it is supposed to terminate the business logic.

    Now, I created the class MyInterface from Excel, it is working fine with the instruction "Run". I then ran "terminate" and set MyInterface = nothing. But it seems that Excel still holds the memory. What should I do to notify the CLR to reclaim the memory?

    Thank you very much!

    Tuesday, June 28, 2011 2:35 AM

Answers

  • Hello XiaoChu,

     

    1. CLR Garbage Collection.

    1.1 When the reference count of the COM-Callable Wrapper for the MyInterface .NET object reaches zero, the .NET object becomes merely eligible for garbage collection. It's actual time of destruction remains non-deterministic. See also section 3 below.

    1.2 You can, however, maintain some level of control by implementing the IDisposable interface for the .NET object and then getting the client code to call the Dispose() method.

    1.3 The following is a sample code :

        [ComVisible(true)]
        [Guid("xxxxxxxx--xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
        [ProgId("WangXiaoChuServer.MyInterface")]
        [ClassInterface(ClassInterfaceType.AutoDispatch)]
        public class MyInterface : IDisposable
        {
            public MyInterface()
            {
            }

            ~MyInterface()
            {
                // If the Finalizer is reached before the
                // IDisposabe.Dispose() method is called,
                // call CleanUp(false).
                CleanUp(false);
            }

            public void Dispose()
            {
                // Tell the GC to not call this object's Finalizer.
                GC.SuppressFinalize(this);
                // Perform all cleanup inside the CleanUp() method.
                CleanUp(true);
            }

            public void Run()
            {
                // Perform Run...
            }

            public void Terminate()
            {
                // Perform Terminate.
            }

            // If bCleanUpCalledFromDispose is true,
            // this method was called from the
            // IDisposable.Dispose() method.
            //
            // If bCleanUpCalledFromDispose is false,
            // this method was called from inside the
            // Finalizer.
            void CleanUp(bool bCleanUpCalledFromDispose)
            {
                // Perform cleanup :
                // e.g. by calling Dispose() method of
                // various business objects.
            }
        }

    1.3.1 One thing you can do inside the CleanUp() method is to call the Dispose() method of the various .NET business logic objects. These, however, must also implement the IDisposable interface.

    1.3.2 The Dispose() method can be useful for releasing various other resources (e.g. file handles, allocated memory, etc). But calling Dispose() does not mean that the relevant object has been garbage collected. Hence they will still reside in memory until the GC kicks in.

    1.3.3 Note also that GC.SuppressFinalize() must be called inside Dispose() so that the actual finalizer is no longer called by the Garbage Collector once Dispose() has been invoked (by the client code).

    1.3.4 The client code must deliberately call the Dispose() method on the object :

    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    MyInterface.Dispose
    Set MyInterface = Nothing

     

    2. An Important Note.

    2.1 Note, however, that in this case, the MyInterface .NET object must not be used by a COM client (e.g. the Excel code) and other .NET clients at the same time (e.g. the same MyInterface object could have been passed by reference to some other .NET object in its lifetime). There would be problems if Dispose() was called prematurely while other objects are still referencing it.

     

    3. Graceful Shutdown of .NET Objects - Often Not Possible.

    3.1 Note that, unfortunately, graceful shutdown of .NET objects (which includes the calling of finalizers) does not occur properly in unmanaged clients unless the CorExitProcess() API is called by the client.

    3.2 I do not know whether the Excel Script Engine calls this API.

    3.3 If you were using VB6, you can link to this API with the following Declare statement :

    Declare Sub CorExitProcess Lib "mscoree.dll" (ByVal exitCode As Long)

    and then call it just before the VB6 app terminates, e.g. :

    Private Sub Form_Terminate()
      CorExitProcess 0
    End Sub

    3.4 See if you can call the CorExitProcess() API in your Excel application.

    - Bio.

     

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Marked as answer by eryang Monday, July 11, 2011 6:34 AM
    Tuesday, June 28, 2011 5:59 AM
  • Hello XiaoChu,


    1. About the IDisposable Interface.

    >> The memory usage keeps rising and ends up with OutOfMemory. So it realy means that GC didn't try to release these memory...

    1.1 Yes of course. The IDisposable interface is not a mechanism for invoking the Garbage Collector at all (see point 1.3.2 of my post dated Tuesday June 28).

    1.2 The purpose of the IDisposable pattern is for clients to ensure timely disposal of important resources (e.g. file handles, network connections, memory allocations, etc) without having to wait for the finalizers (C# destructors) to be called.

    1.3 I had thought that you were only concerned over the freeing of resources. It's clear now that you are generally concerned about the objects being garbage collected.

    1.4 There are 2 options that I can think of that may help you. Please see section 3 below. Section 2 below provides some background to the problem of garbage collection of .NET objects in unmanaged clients.


    2. CorExitProcess().

    2.1 Please refer again to section 3 of my post dated Tuesday June 28 : "Graceful Shutdown of .NET Objects - Often Not Possible."

    2.2 The garbage collection of .NET objects (exposed as COM objects) for unmanaged clients is often not possible. This is so except for newer .NET-aware clients (e.g. those created using Visual C++ .NET 2002 onwards). Client applications created via Visual C++ 6.0, Visual Basic 6.0 and various scripting languages (e.g. VBScript) suffer from this setback.

    2.3 In order to invoke the garbage collector to collect all objects (thus invoking their finalizers), the CorExitProcess() API must be called. This API is exported from mscoree.dll.

    2.4 The disadvantage of this API is that it must be called at the end of your application. Once called, garbage collection will kick in and all finalizers will be invoked. However, once called, your application will terminate.

    2.5 I am not familiar at all with Excel application programming that you mentioned. Does the language allow you to call external APIs ? If so, you can arrage for the CorExitProcess() API to be called at the end of your application. If not sections 3 and 4 may help. Also see MSDN documentation for details on CorExitProcess().


    3. Solution Suggestion 1 - Write an Unmanaged COM Server that calls CorExitProcess().

    3.1 If the Excel langauge does not allow you to call external APIs, then you can write an unmanaged COM server using ATL or Visual Basic 6.0.

    3.2 This COM server can be very simple and provide just one method that will internally call CorExitProcess().

    3.3 The following is an example (code snippet from a COM server written using ATL) :

    STDMETHODIMP CShutdownHelper::ShutdownCLR(LONG exitCode)
    {
      // TODO: Add your implementation code here
      ::CorExitProcess(exitCode);

      return S_OK;
    }

    3.4 Then create an instance of this COM class from your script and call the object's ShutdownCLR() method at the end, e.g. :

    Set Shutdown = CreateObject("CLRHelpers.ShudownHelper")
    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    Set MyInterface = Nothing
    Shutdown.ShutdownCLR 0

     3.5 However, for this solution, the ShutdownCLR() method can only be called at the end of your application.


    4. Solution Suggestion 2 - Call the Garbage Collector On Demand.

    4.1 Although the Garbage Collector is not COM-visible, there is nothing stopping us from creating a COM-visible managed class that exposes the services of the GC.

    4.2 We can certainly create the following managed interface and class :

    [ComVisible(true)]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    public interface IGarbageCollector
    {
      void Collect();
    }

    [ComVisible(true)]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    [ProgId("WangXiaoChuServer.GarbageCollector")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class GarbageCollector : IGarbageCollector
    {
      public GarbageCollector()
      {
      }

      ~GarbageCollector()
      {
      }

      public void Collect()
      {
        GC.Collect();
        GC.WaitForPendingFinalizers();
      }
    }

    4.3 You may then write your script code as follows :

    Set GC = CreateObject("WangXiaoChuServer.GarbageCollector")
    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    Set MyInterface = Nothing
    GC.Collect

    Call the Collect() method of our own COM-visible garbage collector object after you have set MyInterface = Nothing.

    You can perhaps display a message box inside the destructor for the MyInterface class to confirm that it has indeed been called, e.g. :

    ~MyInterface()
    {
      MessageBox.Show("~MyInterface() destructor called.");
    }

    4.4 After calling GC.Collect, your application can still run. It need not terminate like the solution involving CorExitProcess(). We have merely forced the garbage collector to perform a collection on demand.

    4.5 I think this could be a good solution for you.


    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/


    • Marked as answer by eryang Monday, July 11, 2011 6:34 AM
    Sunday, July 3, 2011 10:07 AM

All replies

  • Hello XiaoChu,

     

    1. CLR Garbage Collection.

    1.1 When the reference count of the COM-Callable Wrapper for the MyInterface .NET object reaches zero, the .NET object becomes merely eligible for garbage collection. It's actual time of destruction remains non-deterministic. See also section 3 below.

    1.2 You can, however, maintain some level of control by implementing the IDisposable interface for the .NET object and then getting the client code to call the Dispose() method.

    1.3 The following is a sample code :

        [ComVisible(true)]
        [Guid("xxxxxxxx--xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
        [ProgId("WangXiaoChuServer.MyInterface")]
        [ClassInterface(ClassInterfaceType.AutoDispatch)]
        public class MyInterface : IDisposable
        {
            public MyInterface()
            {
            }

            ~MyInterface()
            {
                // If the Finalizer is reached before the
                // IDisposabe.Dispose() method is called,
                // call CleanUp(false).
                CleanUp(false);
            }

            public void Dispose()
            {
                // Tell the GC to not call this object's Finalizer.
                GC.SuppressFinalize(this);
                // Perform all cleanup inside the CleanUp() method.
                CleanUp(true);
            }

            public void Run()
            {
                // Perform Run...
            }

            public void Terminate()
            {
                // Perform Terminate.
            }

            // If bCleanUpCalledFromDispose is true,
            // this method was called from the
            // IDisposable.Dispose() method.
            //
            // If bCleanUpCalledFromDispose is false,
            // this method was called from inside the
            // Finalizer.
            void CleanUp(bool bCleanUpCalledFromDispose)
            {
                // Perform cleanup :
                // e.g. by calling Dispose() method of
                // various business objects.
            }
        }

    1.3.1 One thing you can do inside the CleanUp() method is to call the Dispose() method of the various .NET business logic objects. These, however, must also implement the IDisposable interface.

    1.3.2 The Dispose() method can be useful for releasing various other resources (e.g. file handles, allocated memory, etc). But calling Dispose() does not mean that the relevant object has been garbage collected. Hence they will still reside in memory until the GC kicks in.

    1.3.3 Note also that GC.SuppressFinalize() must be called inside Dispose() so that the actual finalizer is no longer called by the Garbage Collector once Dispose() has been invoked (by the client code).

    1.3.4 The client code must deliberately call the Dispose() method on the object :

    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    MyInterface.Dispose
    Set MyInterface = Nothing

     

    2. An Important Note.

    2.1 Note, however, that in this case, the MyInterface .NET object must not be used by a COM client (e.g. the Excel code) and other .NET clients at the same time (e.g. the same MyInterface object could have been passed by reference to some other .NET object in its lifetime). There would be problems if Dispose() was called prematurely while other objects are still referencing it.

     

    3. Graceful Shutdown of .NET Objects - Often Not Possible.

    3.1 Note that, unfortunately, graceful shutdown of .NET objects (which includes the calling of finalizers) does not occur properly in unmanaged clients unless the CorExitProcess() API is called by the client.

    3.2 I do not know whether the Excel Script Engine calls this API.

    3.3 If you were using VB6, you can link to this API with the following Declare statement :

    Declare Sub CorExitProcess Lib "mscoree.dll" (ByVal exitCode As Long)

    and then call it just before the VB6 app terminates, e.g. :

    Private Sub Form_Terminate()
      CorExitProcess 0
    End Sub

    3.4 See if you can call the CorExitProcess() API in your Excel application.

    - Bio.

     

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Marked as answer by eryang Monday, July 11, 2011 6:34 AM
    Tuesday, June 28, 2011 5:59 AM
  • Thank you very much, Bio!

    It is my first time to get in touch with this topic. I am reading the books such as, CLR vas C#, and debugging .net 2.0 applications to get familar with it. But I still have problems to understand the issue.

    1. For the .net object created from Excel, who is the root of those objects? Isn't that be the MyInterface object. When we set MyInterface to be nothing, should not all .net ojbects all gone?

    2. I tried to create an delegate class for MyInterface. What the delegate does is that it wraps a MyInterface object in another AppDomain. The delegate class also has a method Instruct. Normally it only pass the instruction to MyInterface. But when the instruction is "TERMIATE", it unload the AppDomain.

    By doing the second way, the memory issue seems to be solved. But who is holding the objects really in the first method?

    Thank you very much!

    Thursday, June 30, 2011 1:59 PM
  • Hello XiaoChu,

     

    1. >> For the .net object created from Excel, who is the root of those objects? Isn't that be the MyInterface object.

    1.1 It is ultimately the .NET MyInterface object that is created and at work.

    1.2 However, they are represented by COM-Callable Wrappers (CCW) in unmanaged code (e.g. your Excel application).

     

    2. >> When we set MyInterface to be nothing, should not all .net ojbects all gone?

    2.1 When you set MyInterface to nothing, the CCW's Release() is called.

    2.2 Note that at this time, there may be other references to the same CCW elsewhere in your Excel application, hence the CCW may still stay alive.

    2.3 When the reference count of the CCW finally reaches zero, it is destroyed and the .NET object that it references is released and becomes eligible for Garbage Collection.

    2.4 Note that some time may need to pass before the garbase collector eventually collect the object. Hence it may hang around for quite a while.

     

    3. >> ... But who is holding the objects really in the first method?

    3.1 One one actually. The objects are now marked for deletion and will be so deleted when the garbage collector decides to dispose of them.

    3.2 Note also that this assumes that your .NET objects (that were exposed to the unmanaged Excel application) are themselves not being referenced by other .NET objects.

    3.3 If they actually are being referenced by other .NET objects, then of course they will not be eligible for garbage collection even when the unmanaged Excel app has stopped using them.

     

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    Thursday, June 30, 2011 2:47 PM
  • Thank you, Bio!

    In my Excel code, the following code is in a loop.

    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    MyInterface.Dispose
    Set MyInterface = Nothing

    The memory usage keeps rising and ends up with OutOfMemory. So it realy means that GC didn't try to release these memory.

    And, Excel is the only user of the .net objects.

    Thursday, June 30, 2011 8:41 PM
  • Hello XiaoChu,


    1. About the IDisposable Interface.

    >> The memory usage keeps rising and ends up with OutOfMemory. So it realy means that GC didn't try to release these memory...

    1.1 Yes of course. The IDisposable interface is not a mechanism for invoking the Garbage Collector at all (see point 1.3.2 of my post dated Tuesday June 28).

    1.2 The purpose of the IDisposable pattern is for clients to ensure timely disposal of important resources (e.g. file handles, network connections, memory allocations, etc) without having to wait for the finalizers (C# destructors) to be called.

    1.3 I had thought that you were only concerned over the freeing of resources. It's clear now that you are generally concerned about the objects being garbage collected.

    1.4 There are 2 options that I can think of that may help you. Please see section 3 below. Section 2 below provides some background to the problem of garbage collection of .NET objects in unmanaged clients.


    2. CorExitProcess().

    2.1 Please refer again to section 3 of my post dated Tuesday June 28 : "Graceful Shutdown of .NET Objects - Often Not Possible."

    2.2 The garbage collection of .NET objects (exposed as COM objects) for unmanaged clients is often not possible. This is so except for newer .NET-aware clients (e.g. those created using Visual C++ .NET 2002 onwards). Client applications created via Visual C++ 6.0, Visual Basic 6.0 and various scripting languages (e.g. VBScript) suffer from this setback.

    2.3 In order to invoke the garbage collector to collect all objects (thus invoking their finalizers), the CorExitProcess() API must be called. This API is exported from mscoree.dll.

    2.4 The disadvantage of this API is that it must be called at the end of your application. Once called, garbage collection will kick in and all finalizers will be invoked. However, once called, your application will terminate.

    2.5 I am not familiar at all with Excel application programming that you mentioned. Does the language allow you to call external APIs ? If so, you can arrage for the CorExitProcess() API to be called at the end of your application. If not sections 3 and 4 may help. Also see MSDN documentation for details on CorExitProcess().


    3. Solution Suggestion 1 - Write an Unmanaged COM Server that calls CorExitProcess().

    3.1 If the Excel langauge does not allow you to call external APIs, then you can write an unmanaged COM server using ATL or Visual Basic 6.0.

    3.2 This COM server can be very simple and provide just one method that will internally call CorExitProcess().

    3.3 The following is an example (code snippet from a COM server written using ATL) :

    STDMETHODIMP CShutdownHelper::ShutdownCLR(LONG exitCode)
    {
      // TODO: Add your implementation code here
      ::CorExitProcess(exitCode);

      return S_OK;
    }

    3.4 Then create an instance of this COM class from your script and call the object's ShutdownCLR() method at the end, e.g. :

    Set Shutdown = CreateObject("CLRHelpers.ShudownHelper")
    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    Set MyInterface = Nothing
    Shutdown.ShutdownCLR 0

     3.5 However, for this solution, the ShutdownCLR() method can only be called at the end of your application.


    4. Solution Suggestion 2 - Call the Garbage Collector On Demand.

    4.1 Although the Garbage Collector is not COM-visible, there is nothing stopping us from creating a COM-visible managed class that exposes the services of the GC.

    4.2 We can certainly create the following managed interface and class :

    [ComVisible(true)]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    public interface IGarbageCollector
    {
      void Collect();
    }

    [ComVisible(true)]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    [ProgId("WangXiaoChuServer.GarbageCollector")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class GarbageCollector : IGarbageCollector
    {
      public GarbageCollector()
      {
      }

      ~GarbageCollector()
      {
      }

      public void Collect()
      {
        GC.Collect();
        GC.WaitForPendingFinalizers();
      }
    }

    4.3 You may then write your script code as follows :

    Set GC = CreateObject("WangXiaoChuServer.GarbageCollector")
    Set MyInterface = CreateObject("WangXiaoChuServer.MyInterface")
    MyInterface.Run
    MyInterface.Terminate
    Set MyInterface = Nothing
    GC.Collect

    Call the Collect() method of our own COM-visible garbage collector object after you have set MyInterface = Nothing.

    You can perhaps display a message box inside the destructor for the MyInterface class to confirm that it has indeed been called, e.g. :

    ~MyInterface()
    {
      MessageBox.Show("~MyInterface() destructor called.");
    }

    4.4 After calling GC.Collect, your application can still run. It need not terminate like the solution involving CorExitProcess(). We have merely forced the garbage collector to perform a collection on demand.

    4.5 I think this could be a good solution for you.


    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/


    • Marked as answer by eryang Monday, July 11, 2011 6:34 AM
    Sunday, July 3, 2011 10:07 AM