all groups > dotnet clr > april 2008 >
You're in the

dotnet clr

group:

A question about life cycle of an object.


A question about life cycle of an object. Xiuming
4/21/2008 9:39:53 PM
dotnet clr:
Hi all.

Below is sample code:
=====================
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;

public class MyClass
{
public static void Main()
{
Test1();
Test2();
}

static void Test1()
{
StringBuilder sb=new StringBuilder();
for (int i=0;i<10000000;i++)
sb.Append("ABCD");
}

static void Test2()
{
Console.ReadLine();
}
}

I build a console application from code above, and I run it for a
test. Then I found that memory held by object "sb" never been
collected by GC unless the application is terminated. Note that sb is
created in Test1 proc, it should be collected after Test1 proc
executed.

Why?
Re: A question about life cycle of an object. Barry Kelly
4/22/2008 1:10:40 PM
[quoted text, click to view]

GC only occurs when you allocate memory, and then, only when the GC
determines that that would, statistically, be a good time to collect. By
statistically a good time to collect, I mean that the current generation
0 isn't large enough to complete the current allocation, which prompts a
GC of generation 0. If that GC frees up enough space for the allocation,
then your string buffer probably won't be deallocated, because it is
probably not in generation 0, being so large and having probably
survived several intermediate GCs in the meantime. Memory gets moved
from generation 0 through generation 1 to generation 2 as GCs of those
generations complete; stuff left over after GC of generation 0 is moved
to generation 1, and so on.

There's lots more information on the .NET GC out there, including MSDN
and MS blogs.

-- Barry

--
Re: A question about life cycle of an object. Barry Kelly
4/24/2008 12:00:00 AM
[quoted text, click to view]

A hashtable with live references to 10,000,000 strings is just as much a
large object graph as a string builder. The situation is no different.
The GC can't collect any of it while the hashtable is rooted. Since lots
and lots of string allocations were needed to add the objects to the
table, a few GCs probably happened in the meantime. But because nothing
could get collected (because everything was kept alive in the hash
table), the memory for the strings and the hash table would have moved
from gen0 to gen2 (I presume you know what these are if you've read
about CLR GC).

Thus, once the hash table is no longer rooted (e.g. nulled out, or the
variable goes out of scope), the GC won't collect it immediately. It'll
have to wait until a gen2 collection for it to get freed up. That won't
happen until both gen0 and gen1 collections occur, which may require
many intermediate allocations, depending on the sizes of gen0 and gen1
(which are based on historical collection rates).

-- Barry

--
Re: A question about life cycle of an object. Xiuming
4/24/2008 2:36:33 AM
Hi Barry.

Before starting this discussion, I have already searched for
informations about how GC works, I create a instance of StringBuilder
class is just for an example.
In fact, I create a instance of Hashtable class and add 10000000
string objects, the same thing happens.

Why?

[quoted text, click to view]
Re: A question about life cycle of an object. Xiuming
4/26/2008 12:04:59 AM
Hi Barry.

I believe that all what you say are perfect right.
My question is about life cycle of an object, after Test1() executes,
Is hashtable object still rooted? Why still rooted?
I ran the process for about 1 day long, the memory kept for the object
(maybe not for the object but for the process) never changed, when
will it be freed?



[quoted text, click to view]

Re: A question about life cycle of an object. Xiuming
4/26/2008 12:13:39 AM
BTW, I missed some information in my post.
When I tested with a hashtable object, I called the Clear() method of
the hashtable instance at the end of Test1() method.
After the Clear() call, references to those string object should be
counted down to 0 I think, and all those string objects should stay in
gen0 and should be collected for a while. But when I check the memory
used by the process after running for 1 hour, it still remains the
same.


[quoted text, click to view]
Re: A question about life cycle of an object. Barry Kelly
4/26/2008 9:59:39 AM
[quoted text, click to view]

The CLR GC currently uses no reference counting for local in-process
objects, AFAIK. There may be special functionality for COM or remoting,
but I don't know the details for those.

[quoted text, click to view]

Take a reference to one of the first strings to you add to the
dictionary and check its generation after the end of the loop, using
GC.GetGeneration, to confirm your theory that it is in gen0. I think you
will find differently, if you do add 10 million unique strings to the
hash table. That would have caused at least one GC, I expect.

[quoted text, click to view]

GC only occurs when memory is being allocated. The GC does nothing if
you are not allocating memory.

First, the GC tries to allocate from gen0. If gen0 has not enough memory
free, the GC collects gen0. This causes memory for live (== rooted)
objects to be copied to gen1, and gen0 becomes clean again; later
allocations will come from gen0.

However, if collecting gen0 didn't free much memory, then the GC will
(probably) collect gen1 (excluding recently copied objects from gen0).
Any memory for live objects in gen1 will be copied to gen2, and gen1
becomes clean again; later collections of gen0 will copy to gen1.

If collecting gen1 didn't free much memory, then the GC will (probably)
collect gen2 (excluding recently copied objects from gen1). Any free
space created in gen2 will be filled up by compacting the heap, that is,
moving the memory for live objects so that they are all contiguous in
memory.

So, if you have an object which has survived many GCs (such as a hash
table that was added to in a big loop, like 10,000,000 items), then all
the memory for it is probably in gen2. In order for it to be freed up by
the GC, a gen2 collection will need to occur. That will need a gen1
collection to have occurred (and failed to free much memory), which in
turn will need a gen0 to have occurred (and failed to free much memory).

So, in order for the hash table data to be freed, you will need to do
more allocations, maybe a lot more. This is good for real applications
because it means the GC doesn't bother collecting until you ask it for
more memory. It is bad for benchmarks, because it seems like the GC
doesn't do much work. You can force a GC, by calling
GC.Collect(generation-number). You can find out which generation an
object is in by calling GC.GetGeneration().

The "probably" in the description above is because the GC may choose to
collect or not to collect based on the historical running of the
program. The idea is that it can tune itself to the running
characteristics of the program.

For example, if it's a batch-mode application, then the most efficient
way is to let loads of memory be used up, then freed all in one go,
ideally by application exit (when GC is free); if it's a long-running
iterative batch-mode application, then peak performance will
theoretically occur when gen0 is allowed to get very large.

Alternatively, if it's a server application with lots of little
requests, then gen0 can be quite small, ideally equivalent to the
maximum working set of a server request. If this were true, then gen1
would only hold objects that were alive during a gen0 collection, but
next time a gen1 collection occurs everything in gen1 dies.

Above all, the GC hopes that it never has to collect gen2, because
collecting gen2 is very slow. The worst pattern for a generational GC to
deal with is an object which gets built up to a large size, so it
survives a few GCs, and then dies. Generation GCs assume that young
objects die fast, and old objects ideally never die.

Of course, if you continue to make allocations (which is the only way
the GC will run by itself), gen2 will eventually get collected.
Otherwise, much memory - in particular, objects allocated from the Large
Object Heap (>80KB in size) would never get collected.

-- Barry

--
Re: A question about life cycle of an object. Barry Kelly
4/26/2008 10:04:08 AM
[quoted text, click to view]

Memory for process is different to memory used for objects, BTW.

Memory allocated, even committed, by a process does not need to take up
space in physical RAM unless the process is using it. I do believe the
CLR GC does return memory to the OS, but I believe it does so more
because of address space exhaustion worries for memory used by native
DLLs that may be loaded by the process, rather than because it's a
useful thing to do in itself.

-- Barry

--
Re: A question about life cycle of an object. Xiuming
4/26/2008 10:44:06 PM
Hi Barry.

I tested it again and found that those objects were in gen2 indeed, as
what you had said. After calling GC.Collect(2), memory used by the
objects was released.
Those objects still exist and will last for very long time, even no
other live objects reference them!
This leads to another question: Is it possible to regain the access to
those objects?

Thanks a lot for your help.


[quoted text, click to view]
Re: A question about life cycle of an object. Barry Kelly
4/28/2008 10:49:30 PM
[quoted text, click to view]

If the objects (or in particular, the root object) has a finalizer, then
you can reassign the root into the global object graph and get it back
that way. However, finalizers make objects a bit more expensive to
allocate, and also cost resources because the GC needs to do more work
to keep track of them. It's better not to throw away an object graph if
you are likely to need it to live for a long time with only intermittent
timespans where it is eligible to become garbage.

Try this app for resurrection:

---8<---
using System;

class Root
{
public static volatile Root Zombie;

public Item Head;
int LifeCount = 2;

~Root()
{
if (--LifeCount > 0)
{
Console.WriteLine("Resurrecting root");
Zombie = this;
}
}
}

class Item
{
public Item Next;
}

class App
{
static void Main()
{
Root root = new Root() { Head = MakeList(10) };
Console.WriteLine("Root generation: {0}",
GC.GetGeneration(root));
GC.Collect(2);
Console.WriteLine("Root generation: {0}",
GC.GetGeneration(root));
root = null;
GC.Collect(2);
GC.WaitForPendingFinalizers();
root = Root.Zombie;
Root.Zombie = null;
Console.WriteLine("Root generation: {0}",
GC.GetGeneration(root));
Console.WriteLine("List length: {0}", ListLength(root.Head));
}

static int ListLength(Item item)
{
if (item == null)
return 0;
return 1 + ListLength(item.Next);
}

static Item MakeList(int count)
{
Item result = null;

while (count > 0)
{
--count;
result = new Item() { Next = result };
}

return result;
}
}
--->8---


-- Barry

--
AddThis Social Bookmark Button