Need explanation of how an object can get garbage-collected when a method is executing on it
In his blog, When does an object become available for garbage collection?, Reymond Chen writes that
An object can become eligible for collection during execution of a method on that very object.
Also, Curt Nichols demonstrates the same point through this example
public class Program
{
static void Main(string[] args)
{
new TestClass().InstanceMethod();
Console.WriteLine("End program.");
Console.ReadLine();
}
}
public sealed class TestClass
{
private FileStream stream;
public TestClass()
{
Console.WriteLine("Ctor");
stream = new FileStream(Path.GetTempFileName(), FileMode.Open);
}
~TestClass()
{
Console.WriteLine("Finializer");
stream.Dispose();
}
public void InstanceMethod()
{
Console.WriteLine("InstanceMethod");
StaticMethod(stream);
}
private static void StaticMethod(FileStream fs)
{
开发者_开发问答 GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("StaticMethod");
var len = fs.Length;
}
}
The output is as expected -
Ctor
InstanceMethod
Finalizer
StaticMethod
ObjectDisposedException is thrown
In this example, I am not able to understand, how GC could collect the temporary TestClass
object since its member stream
was being referred to by the StaticMethod
.
Yes, Raymond states that
GC is not about tracing roots but about removing objects that are no more in use
However, in this example TestClass
object is still being used, isn't it?
Please explain how GC is right in collecting TestClass
object in this case? Also, more importantly, how should developers safeguard against these situations?
I am not able to understand, how GC could collect the temporary TestClass object since its member stream was being referred to by the StaticMethod.
StaticMethod
is in fact not holding onto a reference the TestClass
instance's stream
member - it's holding onto a reference to (as far as the GC is concerned) some FileStream
object on the heap.
Note the default pass-by-value semantics of C#. In this statement:
StaticMethod(stream);
A copy of the value of the stream
field is passed as an argument to the static method. FileStream
is a reference-type and the value of a reference-type expression is a reference. Hence, a reference to a FileStream
object on the heap is passed to the method, not (as you appear to be thinking) a reference / interior reference to a TestClass
object
The fact that this FileStream
object is still reachable when GC.Collect
is called does not result in making the TestClass
object (that created it and has a reference to it through a field) also reachable. "Live" objects make other objects live by referencing them, not by being referred to by them.
Assuming optimizations that result in unneeded references being aggressively "popped off", let's look at the reachability of the created TestClass
object.
Main
doesn't need a reference to it after it calls:InstanceMethod
, which in turn doesn't need the implicitly passedthis
reference after it dereferences it to read thestream
field. The object becomes eligible for collection at this point. It then calls:StaticMethod
, which in turn (as mentioned earlier) isn't holding to a reference to the object at all.
Consequently, the TestClass
object isn't needed right after after its stream
field is read, and is most certainly eligible for collection while StaticMethod
is executing.
This seems to be because when the instance of TestClass calls the StaticMethod it passes by reference the stream object, rather than letting the StaticMethod use the this keyword to access the instance field itself (which would keep a reference to the instance alive).
So as soon as it is passed to the static method by reference the instance can be garbage collected and the stream object disposed of in the Finalizer causing the ObjectDisposedException to be thrown.
I think Curt Nichols explains it pretty well in the blog
Why does this throw? Well, the last live reference to the instance was lost when LongRunningMethod passed _input to Helper. Essentially we've again exported the field value from the instance and no longer hold a reference to the instance, allowing the GC to finalize it. Helper is left holding a reference to an object that has been finalized.
There really is nothing you need to safeguard against. In the test example above, the owning object became eligible for collection, and disposed of an object that it had ownership of. The fact that it passed that object 'outside' means nothing. If you're going to design a class to destroy an object that it claims ownership over, don't pass it back outside without notifying users of that object that it may be destroyed before they are finished using it.
It's important to note that the term "garbage collection" is often used for a number of distinct processes: queueing for immediate finalization, executing a finalizer, and (real) garbage-collection. Objects which have finalizers, or objects that are referred to directly or indirectly by objects with finalizers, are never eligible for real garbage-collection--they're only eligible to be added to the immediate finalization queue. Note that depending upon the parameters used when it's created, a WeakReference may be invalidated either when its referent is queued for finalization, or only when it is "really" garbage-collected.
Note that an object with a finalizer will prevent objects which it directly or indirectly references from being garbage-collected, but it will not prevent them from being finalized. This is important. It means that if your object has a finalizer, when it runs, all finalizable objects to which your class hold direct or indirect references are generally going to be in one of three states:
- Their finalizer may already have run, in which case you don't need to do anything.
- Their finalizer may not have run, but may be queued for immediate finalization once your object's finalizer finishes. Again, no need to do anything.
- If they haven't been finalized or queued for finalization, they're in use. Trying to clean them up will break things.
Very few classes should actually have finalizers (sometimes--annoyingly--called destructors in C#). The only time a class should have a finalizer if it is responsible for performing some cleanup which isn't going to be handled by some other finalizable class. In the event that you do write a class which needs a finalizer, it's probably a good idea to have every method or property in the class include GC.KeepAlive(this) before each return point.
Note: I've often said it's a very bad idea for a derived class to add a finalizer to a base class that doesn't have one. There are a number of reasons, but one that's applicable here is that the base class might not include all the GC.KeepAlives that would be necessary for correct operation.
精彩评论