26

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

https://i.stack.imgur.com/ZhUKc.png

After memory allocation

http ://i.stack.imgur.com/NyXJ6.png

Before leaving the function scope

http ://i.stack.imgur.com/MnTrm.png

After leaving the function scope

http ://i.stack.imgur.com/MiA6T.png

Why did not the garbage collector deallocate the memory allocated by the arrays a, b, c, d even after being set to null?

Community
  • 1
  • 1
Xegara
  • 103
  • 1
  • 7
  • 18
  • 22
    You can't expect a garbage collection to occur _immediately_ after you set null. you can, of course, force it by calling `GC.Collect` to see the difference. – kennyzx Feb 06 '15 at 06:23
  • 2
    Setting those variables to null isn't going to make a difference one way or another given that they immediately fall out of scope. – Preston Guillot Feb 06 '15 at 06:23
  • when will the garbage collector deallocate these memory allocations without calling GC.Collect? – Xegara Feb 06 '15 at 06:25
  • 1
    With an easy language, Why would you need a space to be empty, until you won't need that space to work for. . . .and if you really want to wait for to memory be freed, you can use WaitForPendingFinalizers. – Rohit Prakash Feb 06 '15 at 06:37
  • 31
    If you need deterministic memory deallocation then you're using the wrong language. – Ed S. Feb 06 '15 at 06:38
  • 9
    In general, don't try to understand the garbage collection and don't mess with it. – i know nothing Feb 06 '15 at 09:20
  • 2
    [your think about garbage collection the wrong way](http://blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx) the .net garbage collector simulates infinite RAM. A .net program should be correct if the GC never runs, so long as the computer has enough memory. – user1937198 Feb 06 '15 at 10:15
  • @user1937198 Incorrect I'm afraid. Allocating an array that exceeds available memory will always fail. – Gusdor Feb 06 '15 at 13:28
  • Are you in Release or Debugging Mode? b,c & d should never even be allocated (in R Mode) and thus GC would have nothing have to collect. Debugg Mode works VERY differently from Release mode when we are talking about allocation/deallocation. – StupidOne Feb 06 '15 at 13:32
  • 23
    FYI you are using the wrong tool; looking at total allocation in the task manager tells you almost nothing about how .NET manages memory. If you are interested in watching how the garbage collector works then **use a .NET memory profiler**. That's what it's for. – Eric Lippert Feb 06 '15 at 17:11
  • 3
    @Gusdor: I think you miss the point of user1937198's comment (and Raymond Chen's excellent blog post). **Assuming sufficient memory exists** (which of course is invalid for some extremely large memory allocation, but that's beside the point), then a garbage collector that does nothing at all is completely correct. The purpose of GC is to **reuse memory** that is no longer being used, **not to manage object lifetime**. In an GC environment objects effectively live forever, they just at some point are never used again. Once you stop caring about the object, the runtime can reuse its memory. – Daniel Pryden Feb 06 '15 at 19:58
  • 7
    Why did this question get so much attention? Non-determinism is about the first thing one learns about the .NET GC. This must be the 1000th such question. – usr Feb 06 '15 at 20:49
  • 1
    possible duplicate of [Why do garbage collectors wait before deallocating?](http://stackoverflow.com/questions/17646509/why-do-garbage-collectors-wait-before-deallocating) – Daniel Pryden Feb 07 '15 at 06:55
  • The .NET GC is perfectly deterministic, there's just a lot to know about it. – Jeroen Vannevel Feb 07 '15 at 08:37
  • *"To me, it sounded like a shared pointer equivalent in C++."* -Wrong. *"If the reference counter of an object becomes zero, it will be deallocated by the garbage collector."* -Very wrong. *"...even after being set to null"* -**So** wrong. Garbage collection in .NET has literally nothing in common with C++ memory management. What exactly was written in the document you linked to that could possibly have implied otherwise? – Aaronaught Feb 07 '15 at 08:41
  • 1
    To add to Daniel's comment: It would even be OK to could even collect arrays a,b,c,d **before** they are set to null. You stopped caring about `b`, `c`, and `d` immediately after you declare them (after all, you overwrote them without reading them). – Brian Feb 09 '15 at 14:17
  • 2
    To follow up on my earlier comment: imagine that a process is a huge cafeteria full of tables with no tablecloths. Every now and then people come in and sit down, and the staff puts a tablecloth on the table they sit at and bring them food. People come and go. When they go, the staff removes the plates *but does not remove the tablecloth*, even if the entire table becomes empty of people and plates. Task manager is telling you how many tables have tablecloths, not how many tables have people sitting and eating right now. – Eric Lippert Feb 09 '15 at 23:35

5 Answers5

68

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.

DrKoch
  • 9,556
  • 2
  • 34
  • 43
  • 4
    FYI: The style of garbage collection that you describe here is calling [Tracing garbage collection](http://en.wikipedia.org/wiki/Tracing_garbage_collection). An alternative would be [Refcounting garbage collection](http://en.wikipedia.org/wiki/Reference_counting#Use_in_garbage_collection), where the collector behaves exactly like @EmmettYoung expected. – Radu Murzea Feb 06 '15 at 09:02
  • 1
    @DrKoch *When it reaches about 3-4 GB (depending on many factors) it will suddenly drop to near-zero.* That is not deterministic either. The GC will execute a collection once its generation 0 segment is full and in needs of memory release. With a 64bit process in Server GC mode that wont necessarly happen at 4GB either – Yuval Itzchakov Feb 06 '15 at 09:05
  • 3
    @Yuval Yes, I simplified a bit, of course. Behind the scenes things are **much more complicated**. But I thought my explanations are helpful to get a first, general picture. – DrKoch Feb 06 '15 at 09:07
  • @DrKoch Definitely is useful, but it's important to be *accurate* as well. – Yuval Itzchakov Feb 06 '15 at 09:08
  • 1
    @Yuval My answer contains _"not too much"_ and _"a whole lot"_. This quantities are not accurate at all, still I think this is exactly the information helpful in such an (general, overview-type) answer. – DrKoch Feb 06 '15 at 09:11
  • 1
    @DrKoch Perhaps you could change the "about 3-4" number to something like "begins to consume a significant portion of available physical memory." It is often tempting to throw out "a million" when one really means "a lot." – Moby Disk Feb 06 '15 at 19:15
  • 1
    @DrKoch "The .NET garbage collector [...] and using not too much memory in doing so." Could you give a reference to the first two statements of your answer? – edmz Feb 06 '15 at 19:56
24

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.

pjc50
  • 1,856
  • 16
  • 18
  • 3
    This is a really important point that all the other answers are missing. – jliv902 Feb 06 '15 at 15:22
  • 3
    This really is **the** key. The fact that "Memory usage" reported by Task Manager is not decreasing is *not* evidence that garbage collection is not happening. The same thing can be observed with C or C++ programs using malloc/free or new/delete. The whole question is based on a false premise. – nobody Feb 07 '15 at 22:13
6

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.

Community
  • 1
  • 1
Thomas Weller
  • 55,411
  • 20
  • 125
  • 222
5

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
svasuvelu
  • 51
  • 2
  • I'd say that that "not advisable to use [`GC.Collect()`] explicitly" is slightly too much of a blanket statement. The GC isn't deterministic, but application life-cycles are often predictable. Especially in games (but not only there), calling `GC.Collect` after a "not-too-often-called" section that generates large amounts garbage can improve responsiveness. For example,when loading a large set of objects and discarding another, calling `GC.Collect` before presentation of the new ones can prevent a GC "hiccup" _during_ presenting them, by "offsetting" the collection to loading time. – Selali Adobor Feb 07 '15 at 22:06
5

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653