0

I am trying to write a class that wraps a buffer allocated with Marshal.AllocHGlobal. I implemented the IDisposable interface, and added a finalizer that should release the memory when I don't need it anymore (when the object goes out of scope).

When I test the class, the GC does not call the finalizer or the Dispose method of my classes, even though they are out of scope. As a result, I get an OutOfMemoryException.

Why does the GC not call the finalizer, and why does the memory not get freed?

Here is a short example that illustrates the problem. In the sample, there is nothing written to the console (except Unhandled Exception: OutOfMemoryException.)

class Buffer : IDisposable
{
    public IntPtr buf { get; set; }

    public Buffer()
    {
        buf = Marshal.AllocHGlobal(4 * 1024 * 1024);
    }

    ~Buffer()
    {
        Console.WriteLine("Finalizer called");
        Dispose(false);
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose called");
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    internal virtual void Dispose(bool disposing)
    {
        if (buf != IntPtr.Zero)
        {
            Console.WriteLine("Releasing memory");
            Marshal.FreeHGlobal(buf);
            buf = IntPtr.Zero;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        while(true)
        {
            Buffer b = new Buffer();
            Thread.Sleep(20);
        }
    }
}

EDIT: Here is the .NET performance counters for my test program when it crashes: Performance counters

Ove
  • 6,227
  • 2
  • 39
  • 68
  • You're allocating memory faster than the garbage collector can reclaim it by calling the finalizer of out-of-scope objects. – Martin Costello May 15 '14 at 12:16
  • Is physical memory low when you got out of memory exception? – Matt May 15 '14 at 12:31
  • @Matt No, I have 8 GB of RAM and the test program uses 1.8 GB at its maximum (when it crashes). Even with it using the 1.8GB, I still have around 4.3 GB of free physical memory. – Ove May 15 '14 at 12:33
  • @martin_costello The GC does not even attempt to reclaim anything. Shouldn't it kick in when it notices that the available memory is getting low? – Ove May 15 '14 at 12:35
  • .NET CLR 2 and .NET CLR 4 have different garbage collectors. Which version do you use? – Thomas Weller May 15 '14 at 12:54
  • @ThomasW. I use .NET 4.5 and VS2013, but I switched the target to .NET 2 and I have the same problem. – Ove May 15 '14 at 12:58

3 Answers3

5

You need to tell the garbage collector that your very small managed objects with a single IntPtr field have a high cost in terms of unmanaged memory. Currently, the garbage collector is blissfully unaware of the fact that each small managed object uses a large amount of unmanaged memory and has no reason to perform any collection.

You can use the GC.AddMemoryPressure when you allocate the unmanaged memory and GC.RemoveMemoryPressure when you free the unmanaged memory.

Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
2

Garbage collection occurs when one of the following conditions is true:

  1. The system has low physical memory.
  2. The memory that is used by allocated objects on the managed heap surpasses an acceptable threshold. This threshold is continuously adjusted as the process runs.
  3. The GC.Collect method is called. In almost all cases, you do not have to call this method, because the garbage collector runs continuously. This method is primarily used for unique situations and testing.

Also the garbage collector tracks memory only on the managed heap, so for this program, the only condition can trigger GC is the first one.

I compiled the program, if the target CPU is x86, it will through out of memory exception when the private bytes of the process reaches about 2G. When I run the program, I noticed the private bytes increase quickly, but the working set increase very slowly, and also the system physical memory usage increase very slowly.

As private bytes and working set, this post explains: Private Bytes refer to the amount of memory that the process executable has asked for - not necessarily the amount it is actually using. They are "private" because they (usually) exclude memory-mapped files (i.e. shared DLLs). But - here's the catch - they don't necessarily exclude memory allocated by those files. There is no way to tell whether a change in private bytes was due to the executable itself, or due to a linked library. Private bytes are also not exclusively physical memory; they can be paged to disk or in the standby page list (i.e. no longer in use, but not paged yet either).

Working Set refers to the total physical memory (RAM) used by the process. However, unlike private bytes, this also includes memory-mapped files and various other resources, so it's an even less accurate measurement than the private bytes. This is the same value that gets reported in Task Manager's "Mem Usage" and has been the source of endless amounts of confusion in recent years. Memory in the Working Set is "physical" in the sense that it can be addressed without a page fault; however, the standby page list is also still physically in memory but not reported in the Working Set, and this is why you might see the "Mem Usage" suddenly drop when you minimize an application.

Marshal.AllocHGlobal just increases the private bytes, but the working set is still small, it doesn't trigger GC either.

Please refer this: Fundamentals of Garbage Collection

Community
  • 1
  • 1
Matt
  • 6,010
  • 25
  • 36
0

IDisposable is declarative, The dispose method is only called when Garbage collection actually happens.

Yoy can force garbage collection to happen , for this You need to call

GC.Collect

http://msdn.microsoft.com/en-us/library/xe0c2357(v=vs.110).aspx

I would also recommend using preformance counters to see you app memory consumtion, and see if GC is already called. see here how to do it http://msdn.microsoft.com/en-us/library/x2tyfybc(v=vs.110).aspx

Shachaf.Gortler
  • 5,655
  • 14
  • 43
  • 71
  • It does work if I call `GC.Collect()` inside the loop, but the question is why does the GC not collect automatically when it sees that I'm (almost) out of memory? – Ove May 15 '14 at 12:25
  • You need to use performance counters to see if the GC is indeed called at all, maybe it's not because you still hold reverences to your object for example in some sort of IOC container . – Shachaf.Gortler May 15 '14 at 12:43
  • I have edited my question to provide details of the performance counters for my app. I don't hold other references to the buffers I create, as you can see from the code I posted. The buffer is created then forgotten, not stored anywhere. – Ove May 15 '14 at 12:50