2

It seems some update changed GC behavior when built in Debug configuration or with debugger attached:

//Code snippet 1
var a = new object();
var w = new WeakReference(a);
a = null;

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine(w.IsAlive ? "Alive" : "Dead");

Such code used to print Dead, and it was very handy for writing unit-tests checking that certain parts that should be GCed are not being held.

After some .NET 4.x update, this code passes successfully on .NET 2.x and 3.x, but fails on all variants of 4.x. I tried to call it as GC.Collect(2, GCCollectionMode.Forced, blocking: true), making <gcConcurrent enabled="false"/> in App.config and GCSettings.LatencyMode = GCLatencyMode.Batch - nothing helps. If I run the code without debugger attached and it is built in Release configuration (i.e. with optimizations) - it outputs Dead. Otherwise it is Alive.

I understand that relying on GC is not a good idea in production. But for tests I don't know how to replace ability to check through test that particular code piece does not leak memory. It is pure test assembly, I'm fine with turning some compatibility switches, or something alike. My goal is to check my own code, not the GC optimizations.

Is there a way to force GC to previous behavior somehow?

P.S. I saw almost identical question, but at that time it was related to NCrunch. I don't have it installed. I ran the code even from command line, without VS at all, with the same results.


UPD: I found that if I move code with allocating and setting reference to null into separate method - it consistently outputs Dead, though.

//Code snippet 2
internal class Program
{
    private static void Main(string[] args)
    {
        var w = DoWorkAndGetWeakRef();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Console.WriteLine(w.IsAlive ? "Alive" : "Dead");
        Console.ReadLine();
    }

    private static WeakReference DoWorkAndGetWeakRef()
    {
        var a = new object();
        var w = new WeakReference(a);
        a = null;
        return w;
    }
}

Same result if I move out to separate method GC collection calls and WeakReference check:

//Code snippet 3
internal class Program
{
    private static void Main(string[] args)
    {
        var a = new object();
        var w = new WeakReference(a);
        a = null;

        CollectAndCheckWeakRef(w);
        Console.ReadLine();
    }

    private static void CollectAndCheckWeakRef(WeakReference w1)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        Console.WriteLine(w1.IsAlive ? "Alive" : "Dead");
    }
}

The important point though as it seems is that original w variable is not in current scope. If I move Console.WriteLine(w1.IsAlive ? "Alive" : "Dead"); back to Main - it becomes Alive again.

Both variants are not very convenient sometimes, but at least consistent (Debug or Release configuration, debugger is attached or not - still outputs Dead).

Now I'm curious how mere presence of WeakReference variable in current execution scope prevents GC from cleaning its Target and why having it somewhere in scope buried in call stack doesn't do the same.

Community
  • 1
  • 1
Ivan Danilov
  • 14,287
  • 6
  • 48
  • 66
  • @HansPassant I'm aware about GC's ability to collect unused local variables even if the method is still running. The question here is why it does not collect it even if reference is null-ed and explicitly asked to? I have build agents without latest updates where tests are using it and passing for several years now. And on my local box and on new build agent it is reliably failing for some reason. – Ivan Danilov Sep 30 '15 at 23:12
  • Hmya, focus on posting repro code that prints "Alive" for everybody. – Hans Passant Sep 30 '15 at 23:34
  • @HansPassant [here](https://www.dropbox.com/s/vxoczdbcnvlsf0o/ConsoleApplication2.exe?dl=0) is the binary that prints `Alive` on Win8.1/Win10 with VS2015 installed, and `Dead` on WinServer2008R2 with VS2010/VS2013 and 4.5.1 installed standalone. Built on Win8.1/VS2015 in Debug configuration from the very source in question. Except final `Console.ReadLine()` in the end. (I don't know how to check .NET version exactly - with all of these in-place updates it is not so easy... Can you direct me where to look?) – Ivan Danilov Sep 30 '15 at 23:39
  • Hmm, StrongBox, nice job of hiding that in your snippet. What was the point of that? – Hans Passant Sep 30 '15 at 23:45
  • @HansPassant oops, sorry, it was the version with `StrongBox` suggested by @usr - changes nothing, though, but for the sake of completeness [here](https://www.dropbox.com/s/494gqsrcs1c63am/ConsoleApplication2_without_StrongBox.exe?dl=0) is the original one as well. – Ivan Danilov Sep 30 '15 at 23:46

3 Answers3

1

In Debug mode this is supposed to keep the object alive in all .NET versions. Anything else is a bug (or a missing feature).

You can disable this debug aid by splitting some code off to a fresh method.

In Release mode this should exhibit the short GC lifetimes that you want. That's not guaranteed of course but it's a very desirable optimization.

Another workaround would be to use a new object[1] or a similar construct. You can then null out the first array member reliably. I think the framework has a Box or StrongBox type. Not sure what it's called.

usr
  • 168,620
  • 35
  • 240
  • 369
  • The program should also be run without debugger (Ctrl+F5 instead of F5). – Andrey Nasonov Sep 30 '15 at 22:55
  • Tried both an array and `StrongBox` - same results – Ivan Danilov Sep 30 '15 at 23:15
  • @IvanDanilov maybe the `new object` ends up in a local variable at the JIT level. Move the creation of the box to a factory method. – usr Oct 01 '15 at 08:58
  • Replacing `new object()` with call of `static object CreateObject() { return new object(); }` changed nothing as well. – Ivan Danilov Oct 01 '15 at 10:55
  • @IvanDanilov I just tested both code snippets both in Debug as well as Release. VS13 32bit. All cases work as expected. What scenario is the surprising one to you? – usr Oct 01 '15 at 11:21
  • Do you have .NET 4.6 installed or VS2015? As I said in comments to original question it works the same as you have on two older machines, but doesn't want to after newer bits are installed/updated. – Ivan Danilov Oct 01 '15 at 11:30
  • No 4.6. What scenario is the surprising one to you? I have a 4.6 VM. – usr Oct 01 '15 at 11:31
  • Try the binaries with the original code on the VM and on your box. It will give different results. At least in my case it does. On 4 different machines (WinServer2008R2+VS13, Win7 w/o VS, Win8.1+VS2015, Win10+VS2015) – Ivan Danilov Oct 01 '15 at 11:32
  • I will not run your binaries. I will compile the code fresh. What scenario is the surprising one to you? Debug or Release? Which code snippet? – usr Oct 01 '15 at 11:36
  • Sure, I don't ask you to run my binaries :) Just build the very first snippet of code in question under Debug configuration as a Console Application targeting .NET 4.x (I tried 4.0, 4.5.1, 4.5.2 and 4.6 - no difference, because with in-place update it seems it all are the same binaries). Run it from command line on boxes with and without .NET 4.6 installed and observe different results. – Ivan Danilov Oct 01 '15 at 11:48
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/91097/discussion-between-ivan-danilov-and-usr). – Ivan Danilov Oct 01 '15 at 12:38
  • Just ran the first piece of code ("Code snippet 2"), .net 4.6 VS 15 ctrl-f5 debug. "Dead". Which I expected. – usr Oct 01 '15 at 13:19
  • I thought "first" means "Code snippet 1", doesn't it? :) For "Code snippet 2" I get "Dead" as well, which I wrote in **UPD** – Ivan Danilov Oct 01 '15 at 14:02
  • If you declare the variable in the same method anything could happen. The JIT might introduce locals that keep the object alive. This is to be expected. If you want reliability here use StrongBox. You said this doesn't work. Post the code that does not work but should. – usr Oct 01 '15 at 14:08
  • I don't see how StrongBox makes things any different, but [here](https://gist.github.com/ivan-danilov/c9b49017e1bd43a0f11b) is the example. If I move creation in a factory method - it helps, but the code becomes even worse than snippets 2 and 3. GC behavior was different before, and the question is if it is possible to get it again. Apparently the answer is no, so I don't see a reason to continue. – Ivan Danilov Oct 01 '15 at 14:24
  • Right, StrongBox only helps if the contents are created in a different method. The answer is no, indeed. – usr Oct 01 '15 at 14:40
1

You can't really rely on IsAlive property. The problem with it, is that you can only trust it if it returns false.

Have a look at Why You Shouldn’t Rely On WeakReference.IsAlive

While a WeakReference points to an object that is either live (reachable), or garbage (unreachable) that has not yet been collected by the GC, the IsAlive property will return true. After an object has been collected, if the WeakReference is short (or if the target object does not have a finalizer) then IsAlive will return false. Unfortunately by the time IsAlive returns, the target may have been collected.

This situation can occur because of the way the GC suspends all managed threads before scanning the heap for garbage and collects it (this is an oversimplified explanation for illustrative purposes). The GC can run at any time between two instructions.

The following way is the reliable way to check it.

object a = new object();
WeakReference wr = new WeakReference(a);

object aa = (object)wr.Target;

Console.WriteLine(aa != null ? "Alive" : "Dead");
Community
  • 1
  • 1
CharithJ
  • 46,289
  • 20
  • 116
  • 131
0

The easiest way it seems is to make var a = new object(); a class field instead of a local variable. Makes test less isolated, but seems consistent right now and does not prevent GC from collecting in the middle of the method.

Ivan Danilov
  • 14,287
  • 6
  • 48
  • 66