views:

108

answers:

4

So, I've got this awesome program that is very useful:

static void Main(string[] args)
{
    new Dictionary<int,int>(10000000);

    while (true)
    {
        System.Threading.Thread.Sleep(1000);
    }
}

This doesn't even produce any warnings from the compiler, which is surprising.

Running this allocates a chunk of memory. If I run several copies, I'll eventually get to a point where I can't start any more because I've run out of memory.

  1. Why doesn't the garbage collector ever clean up the memory, instead letting the system get into a state where there is not enough memory for new processes?
  2. Heck, why isn't the memory allocation optimized out? It can never be referenced by anything ever!

So what's going on here?

+6  A: 

The garbage collector is non-deterministic, and responds to memory pressure. If nothing requires the memory, it might not collect for a while. It can't optimize away the new, as that changes your code: the constructor could have side-effects. Also, in debug it is even more likely to decide not to collect.

In a release/optimized build, I would expect this to collect at some point when there is a good reason to. There is also GC.Collect, but that should generally be avoided except for extreme scenarios or certain profiling demands.

As a "why" - there is a difference in the GC behaviour between GC "generations"; and you have some big arrays on the "large object heap" (LOH). This LOH is pretty expensive to keep checking, which may explain further why it is so reluctant.

Marc Gravell
A: 

I don't know this for a fact but I'd guess it's because even without you creating a reference to your new Dictionary, it has been linked to the local scope at that point, which your program never leaves. To check if this is the case, just create the Dictionary in an inner scope which you can leave before starting your loop, e.g.

static void Main(string[] args)
{
    {
        new Dictionary(10000000);
    }

    while (true)
    {
        System.Threading.Thread.Sleep(1000);
    }
}

this should now leave the memory available for Garbage Collection

Tom Carver
er.. why would that make *any* difference.... the IL is **identical** here; there is no local etc - just a "pop". And even if there *was* a variable, IL locals are not scoped like C# variables (they are all declared at the head of the method).
Marc Gravell
Actually, that won't make a slight bit of difference. Those inner statement blocks are practically transparent to the compiler - the resulting IL doesn't see them.
Mark H
+3  A: 

My guess is that a hidden Gen0 collection is being done.

Here is my test program:

static void Main(string[] args)
{
    new Dictionary<int, int>(10000000);
    Thread.Sleep(5000);

    int x = 1; // or 0;
    int i = 0;
    while (true)
    {
        object o = ++i;
        Thread.Sleep(x);
    }
}

When the system executes the Sleep(1), the system must think that this is a good time for a quick, hidden GC on Gen0 only. So the 'object o = ++i' statement never places pressure on Gen0, and never triggers a GC collection and hence never releases the Dictionary.

Sleep(1)

Change x to 0. Now, this hidden GC does not occur, and things work as expected, with the 'object o = ++i' statement causing the Dictionary to be collected.

Sleep(0)

jyoung
nice post, thanks
Marc Gravell
A: 

The GC probably runs and frees the memory... for the application itself. That is, if the Sleep() calls needs to allocate some RAM then it will probably find plenty of it, namely the big blocks which were initially allocated for the huge Dictionary.

This does not mean that the GC gave the memory back to the operating system. From the OS point of view, the big blocks may still be part of the process, not usable by any other process.

The allocation is not optimized out because it is some external code. Your Main class calls out to a constructor for Dictionary<int,int> which could do anything possibly with various side effects. As a human programmer you expect that constructor not to have externally visible side effects, but the compiler and the VM do not know that for sure. So the code cannot dispense with really creating a Dictionary<int,int> instance and calling its constructor. Similarly, the Dictionary<int,int> constructor does not know that it is called for an object which will soon become unreachable, hence it cannot optimize itself out.

Thomas Pornin