質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
87.20%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

解決済

【WPF】ウィンドウを閉じてもメモリ上から破棄されない

twyujiro15
twyujiro15

総合スコア216

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

4回答

5評価

2クリップ

16088閲覧

投稿2019/03/20 04:27

編集2019/03/22 08:11

開発環境

Visual Studio 2013、.NET Framework 4.6 で WPF アプリの開発をしています。

質問内容

下記のようなコードを Release ビルドし、
exe から単体で起動したところ、
ウィンドウを閉じた後に GC をしているにも関わらず、
ウィンドウの生存が確認されました。

最新の動作確認用コードは一番下にあります。
WM_DESTROY を処理しているにも関わらず GC で回収されないことがあります。

XAML

<!-- App.xaml --> <!-- StartupUri プロパティの指定を削除しただけです --> <Application x:Class="WpfApplication3.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Application.Resources> </Application.Resources> </Application>

C#

// App.xaml.cs namespace WpfApplication3 { using System; using System.Windows; /// <summary> /// App.xaml の相互作用ロジック /// </summary> public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); this.ShutdownMode = ShutdownMode.OnExplicitShutdown; var w = new MainWindow(); // 弱参照でウォッチ _r = new WeakReference<object>(w); // ウィンドウを開いて閉じる w.Show(); w.Close(); w = null; this.MainWindow = null; // GC を強制的におこなう GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); MessageBox.Show("GC.Collect() しました。"); // 生存確認 object obj; if (_r.TryGetTarget(out obj)) { MessageBox.Show(obj + " を参照中。"); } else { MessageBox.Show("参照していません。"); } // 終了 this.Shutdown(); } private WeakReference<object> _r; } }

XAML

<!-- MainWindow.xaml --> <!-- 初期状態から何も変更していません --> <Window x:Class="WpfApplication3.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> </Grid> </Window>

あっさり GC してくれると思っていましたが、
それほど単純ではないようです。

さらに上記のコードの内、

C#

//w.Show(); //w.Close();

というように、表示すらしないようにしても GC されないようです。

一体誰がどこで MainWindow を参照し、GC を妨げているのか教えてください。

途中経過

調査している中で、App.Windows プロパティが怪しいかと思いましたが、そうではありませんでした。
このプロパティは「アプリケーションでインスタンス化されたウィンドウを取得」できるようです。
このコレクションは読み取り専用で、クリアしたり各要素に null を代入したりすることができないようです。
ただし、Window.Close() メソッドが呼ばれるとそのコレクションから参照が削除されます。参考ソース
よって、App.Windows プロパティに関しては Close() メソッドさえ呼べば問題ないということがわかりました。


App.Shutdown() を呼んだ後、OnExit() メソッドをオーバーライドして参照を確認したところ、
見事 GC によって破棄されていました。
ソースを確認すると、Dispatcher.BeginInvoke() によって OnExite() メソッドを非同期的に呼び出しているため、これによって後述する状況と同じことになり、たまたま Shutdown() で GC されるようになっただけと言えそうです。


2019/3/22 13:50 現在、皆様のおかげでここまできました。
現在下記のコードにて MainWindow が GC によって回収されることが確認できています。

C#

namespace WpfApplication3 { using System; using System.Threading.Tasks; using System.Windows; /// <summary> /// App.xaml の相互作用ロジック /// </summary> public partial class App : Application { protected override async void OnStartup(StartupEventArgs e) { base.OnStartup(e); this.ShutdownMode = ShutdownMode.OnExplicitShutdown; var w = new MainWindow(); // 弱参照でウォッチ var r = new WeakReference<object>(w); // ウィンドウを開いて閉じる w.Show(); w.Close(); w = null; // 時間をおいてから GC を強制的におこなう await Task.Delay(10); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // WM_DESTROY の確認 MessageBox.Show("IsDestroy = " + IsDestroy); // 生存確認 object obj; if (r.TryGetTarget(out obj)) { MessageBox.Show(obj + " を参照中。"); } else { MessageBox.Show("参照していません。"); } // 終了 this.Shutdown(); } public static bool IsDestroy { get; set; } } }

MainWindow の Close() メソッドを呼んでから GC.Collect() を呼ぶまでの間に、非同期的に時間を置くことで GC されるようになるようです。
上記のコードでは 10[ms] しか待っていないため、10 回に 4 回ほどの割合でしか GC されません。Task.Delay() の時間を(私の環境では 100[ms] に)延ばすとほぼ確実に GC されるようになりました。

このことをもう少し詳しく調査するため、WM_DESTROY を捕捉してそのメッセージが投げられたかどうかを確認しましたが、WM_DESTROY は必ず投げられるようで、上記の "IsDestroy = " のメッセージボックスは常に true となりました。それでも MainWindow が GC されるときとされないときがあることを確認しました。
このときの MainWindow.xaml.cs のコードは次のようになります。

C#

// MainWindow.xaml.cs namespace WpfApplication3 { using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.SourceInitialized += OnSourceInitialized; } private void OnSourceInitialized(object sender, EventArgs e) { // メッセージ処理をフック var handle = (new WindowInteropHelper(sender as Window)).Handle; var hwndSource = HwndSource.FromHwnd(handle); hwndSource.AddHook(WndProc); } private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WM_DESTROY) { App.IsDestroy = true; } return IntPtr.Zero; } private const int WM_DESTROY = 0x0002; } }

まとめ

これまでの調査によって、以下のことが明確になりました。

  1. Window クラスを new するとその参照が Application.Windows コレクションに追加される
  2. Window.Close() メソッドを呼ぶか Application.Shutdown() メソッドを呼ぶとApplication.Windows コレクションから参照が除外される

したがって、new した Window クラスへの参照は、ただ null を代入しても GC されずに残るため、必ず Close() メソッドを呼ぶ必要がある。ただし、「他からの参照」が生き残っているため、Close() メソッドを呼んだだけでは GC の対象にはならない。ある程度時間を(今回の私の環境では 100[ms])置くと「他からの参照」がなくなり、GC の対象となる。

この質問スレッドもだいぶ文章が長くなってきましたので、「他からの参照」というのが何者なのかについてはここでは追究しません。
今後の調査によってはまた質問を投稿させていただきますので、そのときはまた皆様にご協力いただきたいと思います。中々に濃い内容だったように思いますが、これまでお付き合いいただきありがとうございました。

良い質問の評価を上げる

以下のような質問は評価を上げましょう

  • 質問内容が明確
  • 自分も答えを知りたい
  • 質問者以外のユーザにも役立つ

評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

気になる質問をクリップする

クリップした質問は、後からいつでもマイページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

  • プログラミングに関係のない質問
  • やってほしいことだけを記載した丸投げの質問
  • 問題・課題が含まれていない質問
  • 意図的に内容が抹消された質問
  • 過去に投稿した質問と同じ内容の質問
  • 広告と受け取られるような投稿

評価を下げると、トップページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

kikukiku

2019/03/20 05:57

自身のアプリから、自身のアプリのGCを実行しても、起動中なので回収されないのでは?
twyujiro15

2019/03/20 06:10

上記のコードのように、変数 w はあくまでも内部変数なので、このメソッド内の不要になるところから GC の対象になると思っています。が、そうはなっていないようなので投稿した次第です。
S_kawa

2019/03/20 06:14

GC強制のお作法的にはこんな感じに3行書くのが習慣ですね。 GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); //ファイナライズされたものを対象に ※今回のケースに当てはまるかは精査してません 表示云々というよりインスタンス化の時点でイベントハンドラがどうなるかなという気がします。
kikukiku

2019/03/20 06:17

なるほど。知識不足でした。 だとしたら、参照しなくなるまで、待ち続ける必要があるかもです。
twyujiro15

2019/03/20 06:59

S_kawa さんの言うように、確かにお作法的には 3 行が必要かと思いますので質問文のほうのコードも修正させていただきました。イベントハンドラに関して懸念があるとのことですが、具体的に誰が持っているイベント、もしくはハンドラのことなのでしょうか。 作業している中で、そういえば Window クラスのコンストラクタで InitializeComponent() メソッドが呼ばれていることにふと気が付き、少しコードを追いかけて見ていますが、何か参照を保持するようなコードは今のところ見当たりません。というか正直よくわかりません。 kikukiku さん、参考 URL ありがとうございます。こちらのサンプルコードは過去に WeakReference<T> について調べていたときに試したことがありますが、なぜか私の環境では永遠に破棄されないようです。代わりに下記のコードは私の環境でも記事通りに動作しました。 http://pro.art55.jp/?eid=1109086
kikukiku

2019/03/20 07:18

実験の結果をフィードバック頂きありがとうございます。 勉強になります。 ちょっと自身の知識ではお手上げかも。すみません。
wwbQzhMkhhgEmhU

2019/03/20 20:28 編集

多分Window.Close()で発生したClosing/Closedイベントがまだ処理されておらず、普通にWindowが閉じられていないだけだと思います。asyncにしてあげた上で(初期化処理のasyncって気持ち悪いけど)Close直後に await Task.Delay(1000); とか入れてあげれば、UIスレッドがMainWindowのClosing/Closedイベントを処理してくれて破棄されて、弱参照が外れるはずです。 と思ってたのですが、気になってみて書いてみたらClosing/Closedともに呼ばれてました。勘違いでしたすみません。
wwbQzhMkhhgEmhU

2019/03/22 05:10 編集

質問の回答ではなく、回答にも無関係な質問なのですが、質問内容が不明なので一応聞いておきますね。 デフォルトのエントリポイントはMainWindowを作成すると思うのですが、それをさらにOnStartUpで作成するという形はいいのでしょうか? それとも何かMainWindowを作成しない仕組みが組み込まれているのでしょうか? 私は上の検証をしたとき、エントリポイントを作成してApplicationの外で検証した(MainWindowは普通に×ボタンで閉じます)のですが、Application.Runが終わってからgcをかけても回収はされないようでした。 public class EntryPoint { private static WeakReference<object> _r; [STAThread] public static void Main() { Application app = new App(); var w = new MainWindow(); // 弱参照でウォッチ _r = new WeakReference<object>(w); app.Run(w); w = null; // GC を強制的におこなう GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); MessageBox.Show("GC.Collect() しました。"); // 生存確認 object obj; if (_r.TryGetTarget(out obj)) { MessageBox.Show(obj + " を参照中。"); } else { MessageBox.Show("参照していません。"); } } } ※プロジェクトの設定でエントリポイントを変更しないと重複エラーになります
wwbQzhMkhhgEmhU

2019/03/22 05:24

えーっと、MainWindowのコンストラクタ2回呼ばれてませんか?
twyujiro15

2019/03/22 06:01 編集

検証ありがとうございます。 始めに App.xaml にある StartupUri="MainWindow" の記述を削除していますので、App.Run() が実行されても自動的に MainWindow は作成されないはずです。 また、上記のコードで GC されない理由は、Window クラスの Close() メソッドを呼んでいないため、Application.Windows プロパティのコレクションから参照されていることが原因かと思われます。このことから、WPF では new した Window は必ず Show() → Close() しないとメモリリークすると言えると思います。また、Close() するためには必ず Show() しないと例外が発生するため、作法としては一度も Show() する気のない Window は new してはいけないというような感じでしょうか。 何度か質問文を編集している中で、質問内容が不明確となってしまったタイミングがあり申し訳ありませんでした。 --- 「メモリリーク」という表現は不適切でした。開発者側のコードで明示的に new しているのならそれは「リーク」ではなく「キャッシュ」など別の言い回しになりますね。ただ、その「キャッシュ」を破棄する場合、単純に null で開発者側からの参照を抹消するだけでなく、必ず Show() → Close() という手順を踏まないといけないという意味では「メモリリーク予備軍」であると言えると思います。
wwbQzhMkhhgEmhU

2019/03/22 05:32

あ、xamlに書いてあったんですね。気付きませんでした。 Close()ではなく、×ボタンで閉じてるので、リークはしませんよ。 closedイベントでdisposeされています。 また、Delayで待てばgcされるのも同じです。
twyujiro15

2019/03/22 05:39

Closed イベントで何が Dispose されているのでしょうか。また、なぜ時間を置くことで GC されるのでしょうか。具体的に教えていただけないでしょうか。
wwbQzhMkhhgEmhU

2019/03/22 05:49

> Closed イベントで何が Dispose されているのでしょうか。 MainWindowのインスタンスです。 > なぜ時間を置くことで GC されるのでしょうか これは回答ではないと、書いてあるとおりで、正確なところは分かりません。 挙動から見れば、ディスパッチャがまだ処理できてないイベントがあって、それが実行できたときに参照が消えるのではないかと思っています。コードを追うのは面倒なので、追ってませんが、そこには食いつかないでくださいw
twyujiro15

2019/03/22 06:04

>> Closed イベントで何が Dispose されているのでしょうか。 > MainWindowのインスタンスです。 つまり Application クラスから MainWindow のインスタンスが完全に破棄されている、ということでしょうか。 > ディスパッチャがまだ処理できてないイベントがあって そこのところをもっと詳しく知りたいのですが、食いついてはいけませんかw これから私がその部分を調査するに当たって、その入り口というか、ヒントのようなものだけでも教えていただけませんか。
wwbQzhMkhhgEmhU

2019/03/22 06:19

> つまり Application クラスから MainWindow のインスタンスが完全に破棄されている、ということでしょうか。 完全破棄ではないです。IDisposableを実装したDispose()が最低1回呼ばれている状態というだけですね。 gcのタイミングで、finalize可能な状態ということです。 > そこのところをもっと詳しく知りたいのですが、食いついてはいけませんかw > これから私がその部分を調査するに当たって、その入り口というか、ヒントのようなものだけでも教えていただけませんか。 外からのイベントがやってきたのを処理するのはディスパッチャの役目ですが、処理したことになってても、BeginInvokeなどで後回しになってるやつがあるんじゃないか?と思ってるだけです。ただの勘です。
twyujiro15

2019/03/22 06:37

> 完全破棄ではないです。IDisposableを実装したDispose()が最低1回呼ばれている状態というだけですね。 「完全」はちょっと言い過ぎました。失礼しました。 > gcのタイミングで、finalize可能な状態ということです。 質問文のコードでは GC.WaitForPendingFinalizers() を使って行儀よくしていますが、GC.Collect() の 1 行だけでも MainWindow のインスタンスが破棄されることを確認していますので、今回の場合はファイナライザを考慮しなくても良いと思っています。 > BeginInvokeなどで後回しになってるやつがあるんじゃないか? ありがとうございます。例えただの勘であっても調査の糸口をいただけて非常に助かります。
wwbQzhMkhhgEmhU

2019/03/22 14:10

まぁ少し残念な結果になりましたが、コードを血眼で探さなくても分かることだけ調べてみました。 まず、WM_DESTROYを追いかけるコードは意味をなしていません。 Window.Close()はWM_CLOSEメッセージを発行しており、処理上のどこかでWM_DESTROYメッセージ相当も発行されています。 WM_CLOSEはclosingイベントで処理され、WM_DESTROYはclosedイベントで処理されます。 これらは、普通にイベントハンドラを書けば呼ばれるので、そのときのコールスタックを見ることで、Window.Close()由来であることは確認できるはずです。 同時に、WM_DESTROYメッセージのイベントはソースコード上、Window. WmDestroy()で処理されているのも分かるので、ここを見ると、InternalDispose()が呼ばれているので、dispose相当のことがなされていることが分かります(よく見たらIDisposableは実装していませんね)。 次に、そこまで実行しておいて、なぜgcの対象にならないのか?そして時間が経つとなぜgcの対象になるのか、これは明確には分かっていませんが、Dispatcherの残り処理ではないか?と言った根拠だけ書いておきます。 VSのウォッチウィンドウで、 Application.Current.Dispatcher._queue._count を見ていたからです。これをClose直前と直後、及びDelayの直前と直後で見てみてください。 Closeの前後で、増えており、Delayの前後で0になりました。 つまりClose直後にはキューに残っていて未処理のものがあり、それが全部処理されてからgcすると消えているということです。何が積まれているのかは分かりませんが、その中にMainWindowのインスタンスが含まれていたとして、何も不思議はないと思います。なお、AppのディスパッチャもMainWindowのディスパッチャも同じものです。 以上、まだまだ明確ではありませんが、目的も不明な段階でこれ以上の調査は意味がないと考え、私はそこで考えるのを止めました。解決済みになってたので、未提出の中途半端な情報も記述しておいた次第です。 お疲れ様でした。
twyujiro15

2019/03/23 06:21

詳細な補足ありがとうございます。確かにキューが増減することも確認できました。 ここに質問を投稿する前は「WPF ではウィンドウを閉じてもメモリから破棄されないの?」と不安でしたが、「Show()→Close() すればそのうち破棄される」と自信を持って人に説明できるようになりましたので、ここでの議論はお開きとさせていただきました。 今後調査に行き詰ったら別途質問を投稿しますので、見かけたらまたよろしくお願いします。 ありがとうございました。

まだ回答がついていません

会員登録して回答してみよう

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
87.20%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問

同じタグがついた質問を見る

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです