Interesting behavior.

  • Thread starter Thread starter Frank Rizzo
  • Start date Start date
F

Frank Rizzo

Consider the code below. Class mytest is instantiated, then set to
null. In its constructor, the class instantiates a timer, which writes
out 'Hello' every 5 seconds. The odd thing is that even after the
mytest object is killed, 'Hello' is still printed every 5 seconds.

Isn't this a bit odd?

using System;
using System.Threading;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
mytest test = new mytest(); // instantiate an object
test = null; // kill an object

Console.ReadLine();
}
}

public class mytest
{
private Timer timerHello;
public mytest()
{
timerHello = new Timer(
new TimerCallback(timerHello_OnTimer),
new object(), 5000, 5000);
}
private void timerHello_OnTimer(Object stateInfo)
{
Console.WriteLine("Hello");
}
}
}
 
Frank Rizzo said:
Consider the code below. Class mytest is instantiated, then set to
null. In its constructor, the class instantiates a timer, which writes
out 'Hello' every 5 seconds. The odd thing is that even after the
mytest object is killed, 'Hello' is still printed every 5 seconds.

Isn't this a bit odd?

Not at all - because the object isn't killed. It can't be garbage
collected while there are still any live references to it, and the
Timer has a reference to it via the delegate. (I believe that different
timers have different lifetimes, e.g. you need to keep a "live"
reference to a System.Windows.Forms.Timer otherwise it will stop, but I
don't have details.)
 
Jon Skeet said:
Not at all - because the object isn't killed. It can't be garbage
collected while there are still any live references to it, and the
Timer has a reference to it via the delegate. (I believe that different
timers have different lifetimes, e.g. you need to keep a "live"
reference to a System.Windows.Forms.Timer otherwise it will stop, but I
don't have details.)

But isn't this the very kind of circular reference issue that garbage
collection was meant to solve?

Michael
 
Michael,

It's not a circular reference though that this is addressing. Garbage
collection is meant to take care of objects on the managed heap without you
having to worry about them. Because you set it to null, you have indicated
that the object is eligible for collection. However, it is up to the
garbage collector WHEN to actually perform the collection.

An interesting side note here, in release mode, the setting of the
reference to null actually extends the lifespan of the object.

- Nicholas Paldino [.NET/C# MVP]
- (e-mail address removed)
 
[...]
An interesting side note here, in release mode, the setting of the
reference to null actually extends the lifespan of the object.

What does that mean? Is that as opposed to simply letting the reference's
lifetime expire (a containing object gets garbage collected, or a local
variable in a method goes out of scope when the method returns, for
example)? If not, what?

Pete
 
Peter,

In the following code:

static void Main(string[] args)
{
mytest test = new mytest(); // instantiate an object
test = null; // kill an object

Console.ReadLine();
}

The call to "test = null" is actually extending the lifespan of the
mytest instance when this is compiled in release mode. In release mode, the
CLR knows that the reference to mytest is done after the call to initialize
the test variable, and it is eligible for GC. With the addition of the
assignment of null to test, the lifetime is actually extended (it is not
optimized out).


--
- Nicholas Paldino [.NET/C# MVP]
- (e-mail address removed)

Peter Duniho said:
[...]
An interesting side note here, in release mode, the setting of the
reference to null actually extends the lifespan of the object.

What does that mean? Is that as opposed to simply letting the reference's
lifetime expire (a containing object gets garbage collected, or a local
variable in a method goes out of scope when the method returns, for
example)? If not, what?

Pete
 
[...]
The call to "test = null" is actually extending the lifespan of the
mytest instance when this is compiled in release mode. In release mode,
the CLR knows that the reference to mytest is done after the call to
initialize the test variable, and it is eligible for GC. With the
addition of the assignment of null to test, the lifetime is actually
extended (it is not optimized out).

Ah. So it's not really so much that the lifetime of the object is
extended, but that the variable holding the reference to it is (which of
course has the side-effect of extending the lifespan of the object as
well).

Thanks,
Pete
 
Michael,

It's not a circular reference though that this is addressing. Garbage
collection is meant to take care of objects on the managed heap without you
having to worry about them. Because you set it to null, you have indicated
that the object is eligible for collection....

I disagree. Setting a variable to null indicates nothing of the kind.
All that it indicates is that _that variable_ no longer refers to the
object. Whether it is available for collection depends, as you pointed
out, upon whether there are any other references to it.

Since the Timer object has a reference to it, it certainly is not
eligible for collection.

This is a classic oversight, by the way: many programmers (me
included) forget that an event subscription is a reference, and if an
object subscribes to an event then that event subscription can keep
the object alive for longer than you expect. The Timer object, for
example, is keeping the mytest object alive for as long as the Timer
is alive.

A worse example is subscribing to a static event, in which case the
object will never be available for collection, because static events
live for the life of your program (or, more properly, the AppDomain in
which they're loaded).

One way around this is to subscribe to the Timer event via an
intermediate object that uses a WeakReference to refer to the mytest
object. The weak reference doesn't count when determining whether to
collect an object, so the mytest object will in fact eligible for
collection as soon as the variable "test" is set to null (or is no
longer used, as Nicholas pointed out).
 
Nicholas Paldino said:
Peter,

In the following code:

static void Main(string[] args)
{
mytest test = new mytest(); // instantiate an object
test = null; // kill an object

Console.ReadLine();
}

The call to "test = null" is actually extending the lifespan of the
mytest instance when this is compiled in release mode. In release mode, the
CLR knows that the reference to mytest is done after the call to initialize
the test variable, and it is eligible for GC. With the addition of the
assignment of null to test, the lifetime is actually extended (it is not
optimized out).

I disagree. Try the following program:

using System;

class Program
{
static void Main()
{
WithFinalizer f = new WithFinalizer();

Console.WriteLine ("I haven't set f to null yet");
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine ("Setting f to null");
f = null;
Console.ReadLine();
}
}

class WithFinalizer
{
~WithFinalizer()
{
Console.WriteLine ("Collected");
}
}

The output on my box is:
I haven't set f to null yet
Collected
Setting f to null

indicating that the object is collected before f is set to null.
 
Michael C said:
But isn't this the very kind of circular reference issue that garbage
collection was meant to solve?

Not when you've got another thread which has a "live" reference to the
timer. The tricky thing in this particular case is deciding whether or
not another thread (or the thread pool in general) counts. In general,
it certainly would, but as I say I think that you need to keep a
reference to a WinForms timer, but presumably not a
System.Threading.Timer as used here. Of course, I could be barking up
completely the wrong tree and it could be that while a timer is enabled
there's always a reference to it.
 
Jon Skeet [C# MVP] wrote:
to test, the lifetime is actually extended (it is not
I disagree. Try the following program:

using System;

class Program
{
static void Main()
{
WithFinalizer f = new WithFinalizer();

Console.WriteLine ("I haven't set f to null yet");
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine ("Setting f to null");
f = null;
Console.ReadLine();
}
}

class WithFinalizer
{
~WithFinalizer()
{
Console.WriteLine ("Collected");
}
}

The output on my box is:
I haven't set f to null yet
Collected
Setting f to null

indicating that the object is collected before f is set to null.

JWhy is the object collected when there is a live reference to it? I am
confused.
 
JWhy is the object collected when there is a live reference to it? I am
confused.

The JIT compiler is able to ascertain that the variable's value will
never be read after a certain point, so at that point the variable is
no longer a GC root.
 
I agree.
I've learned that generally, setting a reference/variable to null, does not
imply anything concrete.
I've found that GC is quite cold with programmers suggestions (set to null
or other hints).

It is *sometimes* useful for objects in LOH to free space if needed, but
still nothing big.


Jon Skeet said:
Nicholas Paldino said:
Peter,

In the following code:

static void Main(string[] args)
{
mytest test = new mytest(); // instantiate an object
test = null; // kill an object

Console.ReadLine();
}

The call to "test = null" is actually extending the lifespan of the
mytest instance when this is compiled in release mode. In release mode,
the
CLR knows that the reference to mytest is done after the call to
initialize
the test variable, and it is eligible for GC. With the addition of the
assignment of null to test, the lifetime is actually extended (it is not
optimized out).

I disagree. Try the following program:

using System;

class Program
{
static void Main()
{
WithFinalizer f = new WithFinalizer();

Console.WriteLine ("I haven't set f to null yet");
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine ("Setting f to null");
f = null;
Console.ReadLine();
}
}

class WithFinalizer
{
~WithFinalizer()
{
Console.WriteLine ("Collected");
}
}

The output on my box is:
I haven't set f to null yet
Collected
Setting f to null

indicating that the object is collected before f is set to null.
 
Back
Top