12 February 2012

GC.AddMemoryPressure - Working with native resources.

In this writing, I am going to explain how to deal with a situation where an object occupies a large amount of unmanaged memory while consuming very little managed memory. For example, suppose you are using a Bitmap object in your application. The Bitmap application can consume a lot of native memory. But your application just uses the handle to Bitmap which just uses just 4 bytes in 32 bit machines and 8 bytes in 64 bit machines. This means, your application could create several Bitmaps before the garbage collection kicks in. But at the same time, the native memory consumption by the process can increase enormously.

Let me show you an example. In figure A below, I have allocated 2 bitmaps, each of which occupies some big amount of native memory. But you can see that managed heap just containes wrapper to the Bitmaps which occupy very less memory. I will go ahead and create 2 more bitmaps (figure-B). You can see that native memory usage is gradually increasing while there is still enough memory on managed heap. Since, there is enough memory available on the heap, garbage collection doesn't kickin. So, if some more bitmaps are created and garbage collection doesn't occur, then you might run out of native memory which can result in catastrophic failures.



To deal with such problems, System.GC class  provides two static methods - AddMemoryPressure and RemoveMemoryPressure, whose signature is like below.

public static void AddMemoryPressure(long bytesAllocated);
public static void RemoveMemoryPressure(long bytesAllocated);

To know the advantage of these two methods, have a look at the below BitmapObject class. This class is a wrapper around Bitmap. For simplicity, the constructor accepts an image file and constructs a Bitmap.

class BitmapObject
{
    private System.Drawing.Bitmap _bitmap;
    private Int64 _memoryPressure;

    public BitmapObject(String file, Int64 size)
    {
        _bitmap = new System.Drawing.Bitmap(file);
        if (_bitmap != null)
        {
            _memoryPressure = size;
            GC.AddMemoryPressure(_memoryPressure);
        }
    }
       
    public System.Drawing.Bitmap GetBitmap()
    {
        return _bitmap;
    }

    ~BitmapObject()
    {
        if (_bitmap != null)
        {
            _bitmap.Dispose();
            GC.RemoveMemoryPressure(_memoryPressure);
        }
    }
}

Whenever you want to create an instance of System.Bitmap, you can consider creating an instance of above class instead. For instance, I want to create a Bitmap which can have approximately 5MB size. So, I will create an instance of BitmapOject by passing fileName and size as below,

BitmapObject oBitmap = new BitmapObject("c:\\SomePicture.bmp", 5 * 1024 * 1024);

When the constructor executes, it first creates a Bitmap and stores it reference in _bitmap. Then the constructor calls GC.AddMemoryPressure method passing the size (5MB) to it.  This gives CLR a hint of how much native memory is actually occupied by the object. So, though only 4 bytes (or 8 bytes in 64 bit machines) in managed heap, clr assumes that the object actually consumes 5MB. So, suppose Managed heap is of 50MB, then creating 10 instances of BitmapObject makes CLR think managed heap is full and hence it enforces garbage collection. When the garbage collection occurs, the finalizer method executes disposes the bitmap and removes the memory pressure.

3 comments:

  1. Hi Adavesh,

    Can't this be done by simply calling Gc.Collect() to invoke Garbage Collector?

    If it has some other purpose, I would suggest to implement IDisposable pattern instead of the destructor, so that we can use with "using" statement.

    Thanks,
    KPR

    ReplyDelete
  2. Ofcourse you can implement IDisposable and call Dispose & GC.Collect. But, you might know that CLR garbage collector is self tuning. If it sees that there is enough memory and collection won't add anything better, it postpone the next collection. In the above case, the native resource occupies lot of unmanaged memory which is not under GC's control. The managed code has just wrapper around the bitmap which occupies very less memory. So, say 100 Bitmaps occupy just 3.2KB of managed memory which is very less for garbage collection to occur. But at the same time 100 Bitmaps could occupy 200MB of unmanaged memory.
    I hope you are getting what I am telling.

    ReplyDelete