The perils of GC.Collect (or when to use GC.Collect)

I often get asked about the garbage collector (GC) in the .NET Compact Framework. What is this magic thing that handles all my memory for me? Why do I run into out-of-memory situations and GC.Collect sometimes helps? In the past, I have spent some time describing the GC in my performance talks that I have given and I have always thought of it as almost too meaty. However, people are out there writing huge applications and stressing the memory systems to the max (and believe me, I am not complaining about this!). In this article, I continue on this trend to describe the GC in the .NET Compact Framework and when using GC.Collect is appropriate.

 

Steven Pratschner has also written a great article that covers a lot of this and how the .NET Compact Framework garbage collector operates: An Overview of the .Net Compact Framework Garbage Collector

 

Please note that the .NET Compact Framework garbage collector is significantly different than the full .NET Framework. However, the advice for GC.Collect generally remains the same. Please see the following articles from Rico Mariani pertaining to GC.Collect and the full .NET Framework: Two things to avoid for better memory usage and When to call GC.Collect().

Of course, you should re-read Mike Zintel’s article .Net Compact Framework Advanced Memory Management and read his later post "Reaching" Out to 'C' Developers (How I Learned to Love Garbage) which describes “reachability” in a garbage collected runtime.

First off, a couple of definitions:

  • Unmanaged resource – an allocation of any memory block that is invisible to the garbage collector. For example:
    • An application allocates a block of memory in a p/invoke to native code using LocalAlloc.
    • System.Drawing.Bitmap(string fileName) eventually calls CreateDIBSection which will allocate the bitmap on behalf of System.Drawing. The Bitmap class needs to call DeleteObject on this bitmap during collection (finalization). The bitmap is an unmanaged resource.
    • System.IO.FileStream constructor eventually calls CreateFile(…) which returns a handle to represent the file. The handle is an unmanaged resource.
  • OOM  -- an Out Of Memory error. An OOM can surface via a number of ways:
    • OutOfMemoryException thrown
    • Returning a null reference for an object
    • Returning error codes through methods
    • Various other exceptions due to mapping of error codes between unmanaged resources and where the error is returned in managed code

 

Now, why you shouldn’t call GC.Collect:

· The time it takes the CLR to do a GC is a function of both the number of live and dead objects. A GC consists of the following phases:

    • Mark phase: Walk the GC roots to find all live objects and mark them as live.
    • Sweep phase: Walk all of the objects in the GC heap
      • Dead objects are either freed or moved to the finalizer queue if finalization is required.
      • Note that at this point, we may be able to allocate GC objects on the GC heap IFF enough contiguous memory was freed; however, no memory was returned to the operating system.
    • Compaction phase: Move the objects in the heap to reduce the total amount of memory used.
      • The goal of the compaction is to remove all fragmentation in the heap.
      • The GC can not move objects that are pinned. This can be caused by the application explicitly pinning an object or if the object was passed as a parameter to a p/invoke call that is occurring during the GC. Generally speaking, pinned objects are not much of an issue.
      • At this point, the GC will return blocks of free memory to the operating system.
      • Compactions are not performed on every GC; however, they are performed based on a heuristic of heap fragmentation.
    • Pitching JIT compiled code phase: This phase occurs as a last resort and results in all JIT compiled code except for the methods on the current thread stacks to be released. The methods will be recompiled on a demand basis.

· The CLR is self-tuning and knows best when to run a GC. The following triggers are used to invoke a GC in v1:

    • When the CLR fails to allocate required memory via LocalAlloc or VirtualAlloc. This could be caused by the application trying allocate an object or the CLR itself requiring memory.
    • When the System.Drawing subsystem receives an OOM attempting to allocate an unmanaged system resource
      • CreateRectRgn
      • CreateFontIndirect
      • CreatePen
      • CreateSolidBrush
    • The application is moved into the background (causes all four phases to be executed to reduce memory usage to a minimum).
    • When 750KB of objects are allocated from the GC heap since the last GC. This is to help reduce latency by running the GC on a smaller heap.

· Since an application does not have access to the triggers described above, it is difficult to predict when GC.Collect should be called. Consider the following scenario:

    • An application has 2MB of objects allocated.
    • The app frees a large 100KB object and calls GC.Collect. The 100KB is reclaimed.
    • If GC.Collect is called again before any other objects are freed, then it will take the same approximate the same amount of time as the first GC and yet not free any memory.
    • This has the potential of having a serious performance impact on your application.

· Sometimes, GC.Collect is called to force finalizers to be run for dead objects and thus free native code resources or memory. Instead, it is better to implement the IDisposable pattern for objects that have finalizers and then chain the IDisposable pattern up the object chain to the root object. This way, when the application is finished using the root object, it can call Dispose(), which will call the Dispose() methods down the chain to the leaf object and free the unmanaged resource. The garbage collector will handle the rest.

    • A side note: Never implement finalizers for objects that only contain other managed objects. Finalizers will delay when the GC can actually free the memory associated with an object.

 

If you must call GC.Collect, then here are the rules that I would like you to follow:

  • Use GC.Collect to handle OOMs allocating unmanaged resources by trapping the error (a null, error code or exception), calling GC.Collect and the retrying to allocate the unmanaged resource. Unfortunately, in the v1 .NET Compact Framework, there were a few operating system OOMs in System.Drawing that failed to trigger a GC. In this case, you could also handle it as described here.

Here are a couple of samples snippets to show you what I mean. The first is a real piece of code that I used in an application that I built. The application was trying to load a (fairly large) Bitmap from a stream after just releasing the reference to another. The v1 of the .NET Compact Framework does not retry on the CreateDIBSection failure in native code, so this allowed my Bitmap to display correctly if memory was returned to the OS.

 

try {

    // Bitmap constructor throw an exception if OOM

    bitmap = Bitmap(stream);

} catch {

    // Generally, you should try to catch an explicit

    // exception, but I know that this can throw 2 different

    // types of exceptions on an OOM.

    GC.Collect();

    // An exception will be thrown if still OOM

    bitmap = Bitmap(stream);

}

Here is another sample snippet for an application defined unmanaged resource.

class MyObject {

    IntPtr unmanagedResource;

    [DllImport(“myassembly.dll”)]

    private static extern IntPtr GetUnmanagedResource();

    public MyObject()

    {

        // P/Invoke allocates the unmanaged resource and returns

        // null if allocate of the unmanaged object fails.

        unmanagedResource = GetUnmanagedResource();

        // Did the allocation fail?

        if (unmanagedResource == IntPtr.Zero) {

        GC.Collect();

        }

    // Retry...

        unmanagedResource = GetUnmanagedResource();

    // If failed again, propagate the OOM.

        if (unmanagedResource == IntPtr.Zero) {

    throw new OutOfMemoryException();

    }

    }

}

o GC.Collect may not be able to free (and compact) enough memory in order to return memory to the operating system in order for the unmanaged resource allocate to succeed on the second pass.

o Notes:

o You should never need to do this for objects that are strictly managed or if the CLR has triggers associated with the resource.

o This typically should only be done for large resources, certainly not on the order of 100 bytes.

  • You could call GC.Collect after a large object(s) has been freed (> 50KB) and associated with some interactive user event. i.e. a form is closed that had loaded a large dataset to display to the user.

o This restriction of tying it to a user event typically prevents a case of GC.Collect being called in a loop that will cause performance issues.

 

 What we have improved in v2.0 of the .NET Compact Framework:

  • The new threshold for running the GC based on objects allocated since the last GC is 1 MB. This is reasonable since we were able to reduce the latency associated with performing a GC.
  • When WM_HIBERNATE is received by the operating system for low memory notification, the CLR performs a GC.
  • The rest of the out-of-memory conditions for unmanaged resources in System.Drawing that we missed in v1 that should trigger a GC, now do.
    • CreateCompatibleDC
    • CreateCompatibleBitmap
    • Loading a bitmap from file/stream
    • CreateDIBSection
    • CreateFontIndirect
    • CreateRectRgn
    • ImageList_Create
    • ImageList_Add
    • ImageList_AddIcon
  • We have also made the heuristics for performing a compaction to be more aggressive and limit the amount of free memory in the GC heap to 1.5MB.

Scott

 

This posting is provided "AS IS" with no warranties, and confers no rights.