none
TableLayoutPanel内のコントロールを削除(破棄)できずにメモリリークしてしまう RRS feed

  • 質問

  • いつもお世話になっております。
    C#2010で開発しています。

    TableLayoutPanel内にコントロールを配置して利用しているのですが、
    配置したコントロールを破棄したつもりでも、メモリ使用量が減らず、
    メモリリークしているように思われます。

    プログラムの記載に問題があると思うのですが、
    私には原因が分かりませんでした。

    問題を再現できる最低限のプログラムを作ってみました。


            private void buttonAdd_Click(object sender, EventArgs e)
            {
                // 問題の顕在化のため、メモリを多く消費するよう、DataGridViewを100個配置しています
                // 実際には独自に作成したユーザーコントロールを1つずつ配置しています。
                for (int i = 0; i < 100; i++)
                {
                    this.tableLayoutPanel1.ColumnCount += 1;
                    this.tableLayoutPanel1.Controls.Add(new DataGridView(), this.tableLayoutPanel1.ColumnCount - 1, 0);
                }
                System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
            }
    
            private void buttonClear_Click(object sender, EventArgs e)
            {
                // tableLayoutPanel1に配置したコントロールを破棄しています。
                foreach (Control item in this.tableLayoutPanel1.Controls)
                {
                    item.Dispose();
                }
                this.tableLayoutPanel1.Controls.Clear();
                this.tableLayoutPanel1.ColumnCount = 0;
                System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
            }
    


    追加ボタンとクリアボタンを交互にクリックすると、
    次のようにログに記録されて、メモリリークしているように思われます。
    621624
    443600
    807028
    628304
    986876
    808168


    更に引き続き調査を進め、DataGridViewではなくButtonを配置すると、
    メモリリークしていないことが確認できました。

    このことから、DataGridViewは複数のコントロールから構成されているため、
    Control.Disposeでは破棄しきれないのだろうと推測しました。
    (私が作成したユーザーコントロールにも、複数のコントロールが配置されています。)

    このような場合、どのように破棄すれば良いかを教えて頂けませんか。
    教えて頂いた内容を参考に、ユーザーコントロールも破棄できないか
    検証してみたいと思っております。

    以上、よろしくお願い致します。

    2012年7月23日 2:54

回答

  • Fuda1です。

    「TableLayoutPanel.Dispose メソッド 」はどんな処理をしているのでしょうか?ここは、残念ながらシンボルが無いのでソースが見れません。そこで、MSDNライブラリによるとComponent によって使用されているすべてのリソースを解放します。と有ります。更に、Componentの説明には「Component では、Finalize メソッドの暗黙的な呼び出しによる自動メモリ管理を待たずに、Dispose メソッドを呼び出して明示的にリソースを解放する必要があります。Container が破棄された場合は、Container 内のすべてのコンポーネントも破棄されます。」従って、TableLayoutPanel.Disposeメソッドは有効と思われます。

    TableLayoutPanel.Dispose メソッド
    http://msdn.microsoft.com/ja-jp/library/system.windows.forms.tablelayoutpanel.dispose(v=vs.90).aspx

    Componentクラス
    http://msdn.microsoft.com/ja-jp/library/system.componentmodel.component(v=vs.90).aspx

    • 回答としてマーク コンドル 2012年7月24日 7:46
    2012年7月23日 23:44
  • 試してみました。
    GC.Collect を入れるだけではダメで (ジェネレーションを考慮しても挙動変わらず)、
    クリアー後5分待機しても一向にサイズは変わることはありませんでしたが、
    コンテナごと Dispose するとガベージコレクションの対象になるようで、メモリ解放が確認されました。

    以下、元コードに this.tableLayoutPanel1.Dispose(); を加えてるだけです。

    private void buttonAdd_Click(object sender, EventArgs e) {
        // 問題の顕在化のため、メモリを多く消費するよう、DataGridViewを100個配置しています
        // 実際には独自に作成したユーザーコントロールを1つずつ配置しています。
        for (int i = 0; i < 100; i++) {
            this.tableLayoutPanel1.ColumnCount += 1;
            this.tableLayoutPanel1.Controls.Add(new DataGridView(), this.tableLayoutPanel1.ColumnCount - 1, 0);
        }
        System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
    }
    
    private void buttonClear_Click(object sender, EventArgs e) {
        // tableLayoutPanel1に配置したコントロールを破棄しています。
        foreach (Control item in this.tableLayoutPanel1.Controls) {
            item.Dispose();
        }
        this.tableLayoutPanel1.Controls.Clear();
        this.tableLayoutPanel1.ColumnCount = 0;
        this.tableLayoutPanel1.Dispose(); //←追加
        System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
    }

    結果は

    685088
    366328
    685088
    366328
    685088
    366328
    685088
    366328

    となりました。

    個人的には jitta さんの

    >  マネージ クラスが使用しているメモリはランタイムによって管理されているので、人(開発者)が管理する必要はありません。何のためにそれが必要ですか?

    の意見に賛成ですが、どうしても低スペックマシンでスムーズに動作させる必要がある場合などは、考量する必要があるかも知れませんね。


    ひらぽん http://d.hatena.ne.jp/hilapon/


    2012年7月24日 1:29
    モデレータ

すべての返信

  • GC の処理を待機してみてはどうでしょうか?

    GC はバックグラウンドで動作しているため、GetTotalMemory の待機時間だけではメモリの解放まで実施されないことも多いのではないか?と思います。

    一般的なコントロールは、自身の保持するすべてのリソースの破棄を行うべきで、多くのコントロールはそのように実装されているので、DataGridView がどのようになっているかと、作成されているユーザコントロールの話は無縁だと思います。自身の作成したリソースのうち、ライフタイムを管理する必要があるオブジェクトは、void Dispose(boolean) といったメソッドの実装を行って、GC の外から呼び出された場合に全ての管理しているオブジェクトを破棄するのが、一般的によく用いられるコーディングです。


    // 子コントロール(例としてボタン)
    private Button childButton;
    // デストラクタの実装
    public ~MyControl1()
    {
        this.Dispose(false);
    }
    // IDisposable.Dispose の実装
    public void Dispose()
    {
        this.Dispose(true);
    }
    // リソース破棄の実装
    protected virtual void Dispose(bool disposing)
    {
        if (this.components == null)
            throw new ObjectDisposedException();
        if (disposing)
        {
            // 子コントロールを破棄する
            this.childButton.Dispose();
        }
        this.components = null;
        this.childButton = null;
    }
    /*
     * 実際のコントロールでは、親クラスが上記のような実装をもっているので、
     */
    // リソース破棄の実装
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (this.components == null)
            throw new ObjectDisposedException();
        if (disposing)
        {
            // 子コントロールを破棄する
            this.components.Dispose();
            this.childButton.Dispose();
        }
        this.components = null;
        this.childButton = null;
    }
    /*
     * みたいなかんじで、Dispose(bool) の override だけで対応できるようになっています。
     *
     */

    2012年7月23日 3:51
  • K. Takaoka様

    ご回答ありがとうございます。

    > GC の処理を待機してみてはどうでしょうか?
    > GC はバックグラウンドで動作しているため、GetTotalMemory の待機時間だけではメモリの解放まで実施されないことも多いのではないか?と思います。
    System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
    を別のボタンクリックで行うようにして、1分後など待機したあとで行ってみましたが、
    結果は同じでした。
    今回の問題とは直接的には違う問題ですが、メモリが解放されていないように
    思われます。



    DataGridViewとユーザーコントロールは異なるということで、
    確かにその通りだと思いましたので、対象をユーザーコントロールにして、
    調査を続けております。


    まずは、これまでDataGridViewのインスタンスを生成していた箇所を
    UserControl1に変更しました。

            private void buttonAdd_Click(object sender, EventArgs e)
            {
                for (int i = 0; i < 100; i++)
                {
                    this.tableLayoutPanel1.ColumnCount += 1;
                    this.tableLayoutPanel1.Controls.Add(new UserControl1(), this.tableLayoutPanel1.ColumnCount - 1, 0);
                }
            }
    
            private void buttonClear_Click(object sender, EventArgs e)
            {
                // tableLayoutPanel1に配置したコントロールを破棄しています。
                foreach (Control item in this.tableLayoutPanel1.Controls)
                {
                    item.Dispose();
                }
                this.tableLayoutPanel1.Controls.Clear();
                this.tableLayoutPanel1.ColumnCount = 0;
            }
    
            private void button3_Click(object sender, EventArgs e)
            {
                System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
            }
    




    ユーザーコントロールに何も配置していないケースにおいては、
    次のようにログに記載されます。
    初めとほとんど変わっていませんので、メモリリークしていないようです。

    198808
    283212
    198744
    283212
    198744
    283212
    198744


    続いてユーザーコントロールにRichTextBoxを1つ配置してみました。
    同じように検証をすると、結果は次の通りになりました。

    278532
    430248
    353844
    510904
    434500
    585704
    509300


    これだけではメモリリークしてしまうようです(明示的な破棄を記載していない)
    時間を待機しても同じでした。
    (記載することを忘れておりましたが、
     画面は常駐させる画面ですので、画面が閉じた際に解放されても意味がありません。)


    そこでK. Takaoka様に教わったように破棄するコードを
    明示的に記載してみました。

    UserControl1.cs
    Disposeメソッドはデザイナ側のファイルに自動で記載されておりましたので、
    省略しましたが、問題ございますでしょうか?

        public partial class UserControl1 : UserControl
        {
            public UserControl1()
            {
                InitializeComponent();
            }
    
            ~UserControl1()
            {
                this.Dispose(false);
            }
        }
    


    UserControl1.Designer.cs
    richTextBox1をDisposeしたり、nullを代入するように
    Disposeメソッドを変更しております。

        partial class UserControl1
        {
            /// <summary> 
            /// 必要なデザイナー変数です。
            /// </summary>
            private System.ComponentModel.IContainer components = null;
    
            /// <summary> 
            /// 使用中のリソースをすべてクリーンアップします。
            /// </summary>
            /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
            protected override void Dispose(bool disposing)
            {
                if (disposing && (components != null))
                {
                    components.Dispose();
                    this.richTextBox1.Dispose();
                }
                this.richTextBox1 = null;
                base.Dispose(disposing);
            }
    
            #region コンポーネント デザイナーで生成されたコード
    #省略
            #endregion
    
            private System.Windows.Forms.RichTextBox richTextBox1;
    
        }
    



    しかし、結果としては次のように記録されてしまいます。

    195132
    353692
    278436
    278436
    430152
    353748
    356172


    RichTextBox1をうまく破棄できていないようなのですが、
    どのように記載すべきかを教えて頂けませんか?

    お手数お掛けして大変恐縮ですが、よろしくお願い致します。



    2012年7月23日 9:24
  • Fuda1です。

    using System;
    using System.Windows.Forms;
    namespace MemoryTest01
    {
        public partial class Form1 : Form
        {
            private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
            public Form1()
            {
                InitializeComponent();
            }
            private void buttonAdd_Click(object sender, EventArgs e)
            {
                this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
                this.SuspendLayout();
                // 
                // tableLayoutPanel1
                // 
                this.tableLayoutPanel1.ColumnCount = 2;
                this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
                this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
                this.tableLayoutPanel1.Location = new System.Drawing.Point(29, 120);
                this.tableLayoutPanel1.Name = "tableLayoutPanel1";
                this.tableLayoutPanel1.RowCount = 2;
                this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
                this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
                this.tableLayoutPanel1.Size = new System.Drawing.Size(200, 100);
                this.tableLayoutPanel1.TabIndex = 2;
                this.Controls.Add(this.tableLayoutPanel1);
                this.ResumeLayout(false);
                // 問題の顕在化のため、メモリを多く消費するよう、DataGridViewを100個配置しています
                // 実際には独自に作成したユーザーコントロールを1つずつ配置しています。
                for (int i = 0; i < 100; i++)
                {
                    this.tableLayoutPanel1.ColumnCount += 1;
                    this.tableLayoutPanel1.Controls.Add(new DataGridView(), this.tableLayoutPanel1.ColumnCount - 1, 0);
                }
                System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
            }
            private void buttonClear_Click(object sender, EventArgs e)
            {
                // tableLayoutPanel1に配置したコントロールを破棄しています。
                //foreach (Control item in this.tableLayoutPanel1.Controls)
                //{
                //    item.Dispose();
                //}
                //this.tableLayoutPanel1.Controls.Clear();
                //this.tableLayoutPanel1.ColumnCount = 0;
                this.tableLayoutPanel1.Dispose();
                System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
            }
        }
    }

    周辺より、元、tableLayoutPanel1を開放する事で、関連するものも全てメモリ開放しているようです。

    ガベージコレクタのご参考資料 
    http://www.atmarkit.co.jp/fdotnet/dotnettips/021gc/gc.html

    2012年7月23日 10:55
  •  マネージ クラスが使用しているメモリはランタイムによって管理されているので、人(開発者)が管理する必要はありません。何のためにそれが必要ですか?

     Control.Dispose は、マネージ クラスが管理するアンマネージ リソースを解放するために使用します。Dispose を行っても、マネージ メモリが解放されるわけではありません。もし Dispose で解放されるなら、ObjectDisposedException は発生しないでしょう。このページの例外が発生する例では、MemoryStream.Close を呼び出しています。これは、Dispose の別名です。このメソッドを呼び出したからと言って、ms が参照しているメモリが解放されるわけではありません。ms が参照するメモリは、依然として残っています。使用していない(ヒープから参照されていない)マネージ メモリを解放するためには、GC.Collect を呼び出します。

     おっと失礼、GC による回収を待っているのですね。しかし、こう書かれています。「ガベージ コレクターは、アクセスできないすべてのメモリが収集されることは保証していません。」しかし、GC.Collect の方には、そのような記述はありません。また、こちらのサンプル コードで、GetTotalMemory(true) を呼び出す前に GC.Collect が呼ばれています。GC.Collect を入れて、試してみてはどうでしょうか。


    Jitta@わんくま同盟


    • 編集済み Jitta 2012年7月23日 12:11
    2012年7月23日 12:02
  • Fuda1です。

    「TableLayoutPanel.Dispose メソッド 」はどんな処理をしているのでしょうか?ここは、残念ながらシンボルが無いのでソースが見れません。そこで、MSDNライブラリによるとComponent によって使用されているすべてのリソースを解放します。と有ります。更に、Componentの説明には「Component では、Finalize メソッドの暗黙的な呼び出しによる自動メモリ管理を待たずに、Dispose メソッドを呼び出して明示的にリソースを解放する必要があります。Container が破棄された場合は、Container 内のすべてのコンポーネントも破棄されます。」従って、TableLayoutPanel.Disposeメソッドは有効と思われます。

    TableLayoutPanel.Dispose メソッド
    http://msdn.microsoft.com/ja-jp/library/system.windows.forms.tablelayoutpanel.dispose(v=vs.90).aspx

    Componentクラス
    http://msdn.microsoft.com/ja-jp/library/system.componentmodel.component(v=vs.90).aspx

    • 回答としてマーク コンドル 2012年7月24日 7:46
    2012年7月23日 23:44
  • GC によってメモリの解放を待つ手段は提供されていないです。また、GC は頻繁にメモリの確保と解放が実行されている場合、その目安となる量のメモリをキープしておくことで OS に対してメモリの確保や解放によるコストを抑えたりするような挙動もありえると思います。(このあたりは、CLR の ICorなんとかインターフェース経由でカスタマイズしたりできます)

    • GC.WaitForFinalize を呼び出して、ファイナライズキューを空にする
    • アプリケーションをアイコンにする(逆効果かな?

    などなど、GC がメモリを解放しやすい状態にすることはできなくはないですけど、こういった操作がメモリの解放を保証するものではないです。

    2012年7月24日 1:23
  • 試してみました。
    GC.Collect を入れるだけではダメで (ジェネレーションを考慮しても挙動変わらず)、
    クリアー後5分待機しても一向にサイズは変わることはありませんでしたが、
    コンテナごと Dispose するとガベージコレクションの対象になるようで、メモリ解放が確認されました。

    以下、元コードに this.tableLayoutPanel1.Dispose(); を加えてるだけです。

    private void buttonAdd_Click(object sender, EventArgs e) {
        // 問題の顕在化のため、メモリを多く消費するよう、DataGridViewを100個配置しています
        // 実際には独自に作成したユーザーコントロールを1つずつ配置しています。
        for (int i = 0; i < 100; i++) {
            this.tableLayoutPanel1.ColumnCount += 1;
            this.tableLayoutPanel1.Controls.Add(new DataGridView(), this.tableLayoutPanel1.ColumnCount - 1, 0);
        }
        System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
    }
    
    private void buttonClear_Click(object sender, EventArgs e) {
        // tableLayoutPanel1に配置したコントロールを破棄しています。
        foreach (Control item in this.tableLayoutPanel1.Controls) {
            item.Dispose();
        }
        this.tableLayoutPanel1.Controls.Clear();
        this.tableLayoutPanel1.ColumnCount = 0;
        this.tableLayoutPanel1.Dispose(); //←追加
        System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
    }

    結果は

    685088
    366328
    685088
    366328
    685088
    366328
    685088
    366328

    となりました。

    個人的には jitta さんの

    >  マネージ クラスが使用しているメモリはランタイムによって管理されているので、人(開発者)が管理する必要はありません。何のためにそれが必要ですか?

    の意見に賛成ですが、どうしても低スペックマシンでスムーズに動作させる必要がある場合などは、考量する必要があるかも知れませんね。


    ひらぽん http://d.hatena.ne.jp/hilapon/


    2012年7月24日 1:29
    モデレータ
  • Fuda1様

    ありがとうございます。
    tableLayoutPanel1をDisposeすれば、メモリも解放されるのですね。
    参考になりました。
    できたらtableLayoutPanel1はDisposeしたくないところでしたが、
    対応策として検討してみます。



    Jitta様

    > マネージ クラスが使用しているメモリはランタイムによって管理されているので、人(開発者)が管理する必要はありません。何のためにそれが必要ですか?
    今回のアプリケーションの要件として、
    何日間にも渡り24時間1つのフォームを開いておきたいという
    要件があります。
    当該フォームにはtableLayoutPanelが配置されており、
    その中に状況に応じてコントロールを配置したり、削除したりしております。

    削除してもメモリが解放されないと、少しずつの積み重ねではありますが、
    メモリ不足が不足することでスラッシングが発生し、
    アプリケーションのパフォーマンスの低下が発生しております。

    そのため、任意のタイミングでメモリの解放を行いたいと
    思い、今回の質問となりました。



    K. Takaoka様

    GC.WaitForPendingFinalizersするようにしてみましたが、
    うまくいきませんでした。

            private void button3_Click(object sender, EventArgs e)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
    
                System.Diagnostics.Debug.WriteLine(GC.GetTotalMemory(true));
            }
    


    実行結果です
    353844
    510904
    434500
    585704
    509300

    「アプリケーションをアイコンにする」という件は、
    すいません、うまく理解できませんでした。



    ひらぽん様

    ご検証頂きありがとうございます。
    私もtableLayoutPanel1.Dispose();をすることでメモリが解放されることを
    確認できました。
    低スペックマシンというほどではないのですが(メモリ4GB)
    長時間、同じフォームを開き続けるという要件が厳しいため、
    やはりtableLayoutPanel1.Dispose();するよう対応したいと思います。



    皆さま、大変参考になりました。
    ありがとうございました。
    2012年7月24日 7:45
  •  Dispose が解放するのは、というか、Dispose で解放するようにコード化するのは、アンマネージ リソースです。GC.GetTotalMemory が返すのは、確保されているマネージ メモリです。アンマネージ リソースを解放しても、マネージ メモリの確保量が下がるわけではありません。

     で、ここで問題になるのは、TableLayoutPanel に、他のコンテナ コントロールでもいいのですが、配置したコントロールを Controls から削除しても参照しているやつがいる、ということではないでしょうか。「誰も参照していないフォームは、何故 GC の対象にならないのか」(wankuma.com)じゃないですかねぇ?


    Jitta@わんくま同盟

    2012年7月28日 4:44