Thanks, Willy. I understand the part you described (thanks to you, in
another thread), however the part I don't get is how GC.Collect actually
works. You mentioned that when the objects are released, GC collects &
compacts the 16MB sets but does not release those sets to the OS. Is that
what GC.Collect does: just collect & compact, but not release/trim working
set?
If that's the case, how come the Mem Usage column in the Task Manager does
reduce when GC.Collect is executed (and there is no memory pressure)? And
additional question here: how can I signal the CLR to reduce (i.e
release/trim) its original set (since GC.Collect won't do it)?
If that's not the case and GC.Collect does in fact collect/compact and
release/trim, why am I losing 6 MB in the process?
Regards
Ok, let me start with a small correction and a disclaimer. The disclaimer
first, what I'm talking about is valid for v1.x and only for the workstation
version of the GC. The correction is that at the start the CLR reserves 2
segments of 16 MB (each having 72kb committed) for the gen0-2 heap plus a 16
MB segment for the LOH.
Consider following (console) sample and say we break at 1 2 and 3
respectively to take a look at the managed heap:
int Main() {
[1]
ArrayList [] al = new ArrayList [1000000];
for (int m = 0; x < 1000000; m++)
al[m] = new ArrayList(1);
[2]
for (int n = 0; n < 1000000; n++ )
{
al[n] = null;
}
GC.Collect();
[3]
At the start [1] of a (CLR hosted) process the GC heap looks like this:
|_--------------|_----------------|......................|---------------|
S0 S1 free
LOH 16MB
S0 = 16MB - 72kb Committed regions (_)
S1 = 16MB - 72kb Committed regions
objects allocated at the start of the program fits in the initial committed
part of the S0 segment, so this committed region contains gen0, 1 and 2.
Say the number of reachable objects account for 6kb heap space here.
When we break at 2, the heap has grown such that S0 and S1 are completely
filled (committed regions) and a third segment had to be created.
|______________|_______________|....|________------|....|---------------|
S0 S1 S2
LOH 16MB
S0 = 16MB - x MB Committed regions
S1 = 16MB - y MB Committed regions
S2 = 16MB - z MB Committed regions
S0 and S1 contains Gen2 objects (those that survived recent Collections)
S2 now holds Gen1 and Gen0
Total object space ~42Mb
Let's Force a Collection and break at 3, now the heap looks like:
|_---------------|____------------|....|_____----------|....|---------------|
S0 S1 S2
LOH 16MB
S0 = 16MB - x MB Committed regions
S1 = 16MB - y MB Committed regions
S2 = 16MB - z MB Committed regions
Total object space = what we had at [1] (6kb), but as you notice the CLR
didn't de-commit all regions and didn't return segment S2 to the OS.
The amount of non de-committed region space depends on a number of
heuristics like; the allocation scheme and the frequency of most recent
object allocations.
When you run above sample you'll see that x, y, z accounts for ~10Mb (your
mileage may vary of course), so when you look at the working set of the
process, you'll notice a growth of ~10MB too. So say we started with 6MB at
[1], we will see 16MB when we are at [3].
What you could do (but you should never do that) is try to reduce the
working set of the process by setting the Process.MaxWorkingSet property,
note that this will not change the heap lay-out and will not return anything
to the OS, only thing that is done is force a page-out of unused process
pages.
Changing the committed region space and the allocated segment space is in
the hands of the CLR and the OS, both of them know what to do and when much
better than you do so keep it that way, after all this is why GC memory
allocators are invented right?
Willy.