0

This is a simple test application to help understand WPF memory usage. They key thing I want to understand is why MainWindow is still referenced and it's memory not released, even after being closed and having waited for GC finalization?


(See code listing below)

The text "MainWindow finalizer" is not executed by the time of snapshot #2, which seems unexpected. To investigate, I have taken two memory snapshots using VS diagnostic tools at the points indicated in the code listing.

Here's the VS comparison of the two snapshots:

enter image description here

This shows that the MainWindow is still around. But why, if nothing is referencing it? Drilling down (again using the diagnostic tools) it turns out that there is a reference after all:

enter image description here

There are other objects also referencing the MainWindow, but they all eventually form a cycle back to it, so I do not think they are genuinely "root" objects which are keeping the reference alive. But for the MediaContext / Dispatcher duo this is not the case.

The Dispatcher as I understand it is run once per thread so that seems OK by itself. But what's up with the MediaContext that it owns, which in turns holds onto my MainWindow?

Is this normal? Is it a "memory leak"? Why does it happen?

Also, importantly how can I / should I actually get rid of the MainWindow object?


App.xaml:

<Application
    x:Class="memtest.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:memtest"
    StartupUri="MainWindow.xaml"
    Startup="Application_Startup"
    >
    <Application.Resources/>
</Application>

App.xaml.cs:

namespace memtest
{
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            // *** SNAPSHOT 1 ***

            ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;

            MainWindow window = new MainWindow();
            window.Show();
            window.Close();
            window = null;

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            // *** SNAPSHOT 2 ***
        }
    }
}

MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Debug.WriteLine("MainWindow constructor");
    }

    ~MainWindow()
    {
        // Never reached
        Debug.WriteLine("MainWindow finalizer");
    }
}

MainWindow.XAML is the default created by VS, which contains only an empty grid.

There is no other code in the project.

This is a .NET 4.72 project.


This is not quite a dupe of A WPF window doesn't release the memory after closed, because it did not use WaitForPendingFinalizers() nor did it use an explicit finalizer. And that question has no valid answers.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
  • Does this answer your question? [WPF window wont release its resources untill program terminates](https://stackoverflow.com/questions/60692407/wpf-window-wont-release-its-resources-untill-program-terminates) – Peter Duniho Aug 26 '20 at 01:37
  • _"why MainWindow is still referenced and it's memory not released, even after being closed and having waited for GC finalization?"_ -- you already know, because you looked at the memory profile. This is the same thing I found when I was investigating the proposed duplicate. For whatever reason, WPF does not correctly unhook the `Window` object from its infrastructure, leaving it reachable and thus not garbage-collectable. This is normally not a problem, but if it's creating issues with memory pressure for you, the solution is to reuse window instances. Again, per the duplicate. – Peter Duniho Aug 26 '20 at 01:39
  • @PeterDuniho thanks for pointing out that other question, which indeed has a lot of similarities. It only has a workaround though, not really a comprehensive answer as to why this behavior occurs or what the WPF designers expected you to do about it. Though I realize that perhaps such a comprehensive answer may be unknown. – StayOnTarget Aug 26 '20 at 12:33
  • _"not really a comprehensive answer as to why this behavior occurs or what the WPF designers expected you to do about it."_ -- you're unlikely to get an answer like that. If you want to know what the WPF designers expect, you need to ask them, and they aren't generally responding here. In any case, to me the issue seems more like a bug, so it's doubtful they _intended_ the behavior in the first place. It's rare to get any sort of "comprehensive" discussion of implementation details of frameworks on SO...you should never expect that as the outcome for a question. – Peter Duniho Aug 26 '20 at 16:51
  • @PeterDuniho I appreciate your advice and understand that caliber of an answer is probably unlikely. On the other hand I somewhat disagree, because SO has many questions with incredibly in depth and comprehensive answers on topics similar to this in nature (ie not dealing with the OP's own code), and certainly plenty in the .NET sphere. Many (now older) answers by Hans Passant, Jon Skeet, or Eric Lippert, to note some particular luminaries I've come across. I guess all I mean to say is that I'm an optimist :) – StayOnTarget Aug 26 '20 at 16:55
  • @PeterDuniho just to add, it didn't seem far-fetched to me that someone else has come across this issue and already knows of a solution or explanation. Certainly seemed worth asking, anyway! – StayOnTarget Aug 26 '20 at 16:57
  • _"it didn't seem far-fetched to me that someone else has come across this issue and already knows of a solution or explanation"_ -- that's not far-fetched at all. That's why someone indeed _has_ come across this issue and already knows of a solution (or at least, work-around), and an explanation (the `Window` object continues to be made reachable via static WPF data structures). The latter you already knew about though, and probably would've figured out the former eventually, had there not already been an answer to that here. – Peter Duniho Aug 26 '20 at 17:54
  • Given that the issue is likely a bug, I doubt there's much more to the explanation than what you already determined via the memory profile, which is the same as what I already determined the last time the question was asked. Since the behavior is likely unintentional, I don't know how much more in-depth an answer would even be useful. We already know the `Window` object isn't going to get collected, and so reusing it is the best way to avoid accumulating a large number of them. (No code should ever rely on finalization, so that aspect is a complete non-issue.) – Peter Duniho Aug 26 '20 at 17:56
  • @PeterDuniho you are probably right. If somehow it turned out not to be a bug... well that would have an interesting explanation! The finalizer was only there for debugging purposes. – StayOnTarget Aug 26 '20 at 17:58

1 Answers1

1

This test is making several mistakes, also explained here

  • you are taking the second snapshot while the MainWindow variable is still on the stack frame. JIT is allowed to optimize your assignment to window = null; away because it can clearly see the variable is not used anymore afterwards. Furthermore GC reporting of stack frames is not exact (with respect to your source), there may be hidden copies on the stack. Move the test code into a separate method which you return from, to make sure no references to MainWindow are left on the stack. (Technically not necessary after fixing the next point, but I'm mentioning it for completeness so people understand that point when writing GC tests.)
  • you are not giving the multithreaded WPF rendering engine time to clean up, closing and forcing GC is not enough to synchronize with the rendering engine to clean up its resources
  • you are leaving StartupUri="MainWindow.xaml" in the app, remove it to make testing with the fixed code simpler

The correct way to perform your test is to start a DispatcherTimer and take the second snapshot there, for me the MainWindow is gone then.

private void Application_Startup(object sender, StartupEventArgs e)
{
    // *** SNAPSHOT 1 ***

    ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;

    RunTest();

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, Callback, Dispatcher).Start();
}

private void Callback(object sender, EventArgs e)
{
    // *** SNAPSHOT 2 ***
}

private static void RunTest()
{
    MainWindow window = new MainWindow();
    window.Show();
    window.Close();
    window = null;
}
Zarat
  • 2,584
  • 22
  • 40
  • Thanks for these tips, I will try them out! – StayOnTarget Aug 30 '20 at 12:14
  • The timer waits 1 second - I assume that is just arbitrarily long enough? Is there any way to determine conclusively when the "cleanup" is done? An event or something? – StayOnTarget Aug 31 '20 at 13:32
  • 1
    Yes 1 second is arbitrary, just giving WPF some time to clean up. If you want a notification the best you can do is putting a finalizer somewhere, either on your window or on something referenced by the window. – Zarat Sep 01 '20 at 22:26