I have tried a simple experiment to verify the functionality of the garbage collector. Referencing 3.9 Automatic memory management (MSDN) about automatic memory management in .NET. To me, it sounded like a shared pointer equivalent in C++. If the reference counter of an object becomes zero, it will be deallocated by the garbage collector.
So I tried creating a function inside my main form. The function was called inside the Shown event function of my main form which is executed after the constructor. Here is the experimental code.
public void experiment()
{
int[] a = new int[100000];
int[] b = new int[100000];
int[] c = new int[100000];
int[] d = new int[100000];
a = null;
b = null;
c = null;
d = null;
}
And here are the results:
Before memory allocation
After memory allocation
Before leaving the function scope
After leaving the function scope
Why did not the garbage collector deallocate the memory allocated by the arrays a, b, c, d even after being set to null?
The .NET garbage collector is an highly optimized, complicated beast of software. It is optimized to make your program run as fast as possible and using not too much memory in doing so.
Because the process of freeing memory takes some time, the garbage collector often waits to run it until your program uses a whole lot of memory. Then it does all the work at once, which results in a small delay of your program after a relatively long time (instead of many smaller delays earlier, which would slow down your program).
All this means, that the time the garbage collector runs is not predictable.
You may call your test several times (with some Sleep() in the loop) and watch memory usage slowly building up. When your program begins to consume a significant portion of available physical memory its memory usage will suddenly drop to near-zero.
There are a couple of functions (like GC.Collect()
) which force several levels of garbage collection, but it's strongly advised not to use them unless you know what you are doing, because this tends to make your software slower and stops the garbage collector in doing its work in an optimal way.
Even if it did de-allocate the memory internally, it's under no obligation to return it to the operating system. It will assume that more memory will be requested in the future and recycle the pages. The operating system's number knows nothing of how the program has chosen to use the memory it has claimed.
If you actually want to claim and release memory explicitly you'll have to call VirtualAlloc() through Pinvoke unsafe code.
The CLR does not run the garbage collector for every memory release as it consumes system resources. So the garbage collector is called at regular intervals based on the growing memory size. It would clear all the unrefered memory leaks.
Also the garbage collector can be called explicitly using the method GC.Collect(), but it is not advisable to use explicitly.
Garbage collection is expensive. You only want it to run as seldom as possible. Ideally never. Therefore, the system will try delaying garbage collection as long as it can, basically until you run out of memory.
Allocating memory is expensive. Once the runtime has allocated some memory, it will typically not free it again, even if it doesn't currently need it, because if it needed that much memory during one time of the runtime of the program, it is likely that it will need similar amounts of memory at some time in the future and wants to avoid having to allocate memory again.
So, even if garbage collection occurred during your test, you wouldn't see it in the Task Manager or Process Explorer, because the CLR wouldn't free it anyway.
What you are describing is called a reference-counting garbage collector. However, all currently existing implementations of the CLI VES use a tracing GC. Tracing GCs don't count references; they trace them, only when they are running. A tracing GC will not notice whether an object is still reachable or not until it actually traces the object graph, and it will only trace the object graph when it needs to run a collection, i.e. when you run out of memory.
Some of the information is already included in the article you link to. There are several indications that the behavior you observe is correct:
... the garbage collector may (but is not required to) treat the object as no longer in use.
... at some unspecified later time ...
GC.Collect()
One important thing, at least for the old (non-concurrent) version of the Garbage collector is, that the garbage collector runs on a different thread. You can verify that in the debugger:
0:003> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 1b08 0058f218 a020 Enabled 025553ac:02555fe8 0058b868 1 MTA
2 2 1e9c 005a78c8 b220 Enabled 00000000:00000000 0058b868 0 MTA (Finalizer)
The Finalizer thread performs the garbage collection. All other threads are suspended during the operation, so that no thread can modify objects during the time of reorganization.
But why is that important?
It explains why the garbage collection does not apply immediately, neither in your scenario nor if you call GC.Collect()
to do the garbage collection. For the garbage collector to run, you also need a thread switch. So, the minimum code needed for a non-concurrent garbage collection is
GC.Collect();
Thread.Sleep(0);
If you're concerned about memory management, be sure to also check out the awesome answer about IDisposable.
Free memory
Also, nobody has explained yet, that looking at the memory consumption with Task Manager is not reliable.
.NET acts directly on virtual memory, i.e. uses the virtual memory manager. It does not use the heap, i.e. the heap manager. Instead it uses it's own memory management, called managed heap.
.NET gets the memory from Windows (the kernel). Assume it gets a fresh piece of memory from Windows, which has no .NET objects inside. From Windows' point of view, the memory is gone (given to .NET). However, from .NET point of view, it's free and can be used by objects.
Again, you can observe that in the debugger:
0:003> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 60 71cb9000 ( 1.778 Gb) 88.91%
<unknown> 84 986f000 ( 152.434 Mb) 67.09% 7.44%
Image 189 2970000 ( 41.438 Mb) 18.24% 2.02%
...
What is reported as <unknown>
is virtual memory from Windows point of view. In this case, 150 MB are used.
0:003>!dumpheap -stat
...
00672208 32 8572000 Free
...
So you can see that 8.5 MB are free from .NET point of view, but have not been given back to Windows (yet) and will still be reported as used there.
Measuring working set
If you have not modified Task Manager's default column settings, it's even worse, because it will show the Working Set, which is memory in RAM only. However, some of the memory may have been swapped to disk, thus it may not be reported by Task Manager.