- PR -

Form上のコントロールのDispose()について

投稿者投稿内容
未記入
大ベテラン
会議室デビュー日: 2008/02/07
投稿数: 115
投稿日時: 2008-10-01 21:41
引用:

バグではない理由:
参照していると、回収されません。Dispose は、メモリの破棄ではありません。


参照しているというのは fm2 のことでしょうか。fm2 はループ内で宣言されているので、ループ 1周ごとにスコープを抜ける、つまり new Form2() は参照されていない状態になるのではないでしょうか?

GC.Collect() で直前の fm2 が解放されるとは思っていませんが、ループ 1周遅れで、ひとつまえの new Form2() インスタンスは GC で回収されても良さそうに思います。直近の 1インスタンスは GC.Collect() で回収されないと思いますが、それだけでメモリ使用量が増加傾向になるものでしょうか?
よねKEN
ぬし
会議室デビュー日: 2003/08/23
投稿数: 472
投稿日時: 2008-10-01 22:09
引用:

未記入さんの書き込み (2008-10-01 21:41) より:
引用:

バグではない理由:
参照していると、回収されません。Dispose は、メモリの破棄ではありません。


参照しているというのは fm2 のことでしょうか。fm2 はループ内で宣言されているので、ループ 1周ごとにスコープを抜ける、つまり new Form2() は参照されていない状態になるのではないでしょうか?



これについてはそのとおりだと思います。

引用:

GC.Collect() で直前の fm2 が解放されるとは思っていませんが、ループ 1周遅れで、ひとつまえの new Form2() インスタンスは GC で回収されても良さそうに思います。直近の 1インスタンスは GC.Collect() で回収されないと思いますが、それだけでメモリ使用量が増加傾向になるものでしょうか?



件のコードでは変数fm2でForm2のインスタンスの参照を保持したまま、
GC.Collect()しているので、このインスタンスのジェネレーションが上がってしまい、
回収されにくいインスタンスになりますので、メモリに居座り続ける可能性が高いと思います。

ジェネレーションについては以下の文章の後半で簡潔な解説があります。
http://www.atmarkit.co.jp/fdotnet/dotnettips/021gc/gc.html


より詳しい話は以下のURLとか。
http://www.microsoft.com/japan/msdn/net/mag00/GCI.aspx
http://www.microsoft.com/japan/msdn/net/mag00/GCI2.aspx

[ メッセージ編集済み 編集者: よねKEN 編集日時 2008-10-01 22:10 ]
未記入
大ベテラン
会議室デビュー日: 2008/02/07
投稿数: 115
投稿日時: 2008-10-01 22:28
引用:

件のコードでは変数fm2でForm2のインスタンスの参照を保持したまま、
GC.Collect()しているので、このインスタンスのジェネレーションが上がってしまい、
回収されにくいインスタンスになりますので、メモリに居座り続ける可能性が高いと思います。


ジェネレーションの影響を受けるのは GC の自動回収ではないでしょうか。GC.Collect() では強制的に、ジェネレーション2までのガベージが回収されるのでありませんでしたっけ?
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2008-10-01 23:18
引用:

未記入さんの書き込み (2008-10-01 21:41) より:
引用:

バグではない理由:
参照していると、回収されません。Dispose は、メモリの破棄ではありません。


参照しているというのは fm2 のことでしょうか。fm2 はループ内で宣言されているので、ループ 1周ごとにスコープを抜ける、つまり new Form2() は参照されていない状態になるのではないでしょうか?

GC.Collect() で直前の fm2 が解放されるとは思っていませんが、ループ 1周遅れで、ひとつまえの new Form2() インスタンスは GC で回収されても良さそうに思います。直近の 1インスタンスは GC.Collect() で回収されないと思いますが、それだけでメモリ使用量が増加傾向になるものでしょうか?


よねKENさんのフォローと同じだと思いますが。
引用:

オリジナルのコード
コード:
while (true) 
{ 
    Form2 fm2 = new Form2(); 
    fm2.ShowDialog(); 
    fm2.Dispose(); 
    GC.Collect(); 
}



GC.Collect が実行されるときには fm2 は参照されています。従って、1つは残ります。

で、ループになっているところには、気づいていませんでした。
GC.Collect() は、全てのジェネレーションに対してコレクトを行いますから、1つは残りますが、全てが残ったままとなるというのは、おかしいです。
ただ、GC が管理するものと IDisposable で管理を任されるものは異なりますから、Dispose を入れたから挙動が変わるというのは違うと思います。



あと、こんな現象があるそうです。
Compact Framework のメモリ管理<live.com>(宇宙仮面のC#研究室)

キャッシュされるのでメモリ使用量が増えていくというのはわかるのですが、それが OutOfMemory を引き起こすまでとなると、確かにおかしいですね。
よねKEN
ぬし
会議室デビュー日: 2003/08/23
投稿数: 472
投稿日時: 2008-10-02 10:07
引用:

未記入さんの書き込み (2008-10-01 22:28) より:
ジェネレーションの影響を受けるのは GC の自動回収ではないでしょうか。GC.Collect() では強制的に、ジェネレーション2までのガベージが回収されるのでありませんでしたっけ?



MSDNのGC.Collect()(引数なし)のドキュメントの記述によれば全ジェネレーションが対象となる
とありますので、この記述からするとその通りだと思います。

と同時に「 ただし、Collect メソッドは、アクセスできないすべてのメモリが収集されることは保証していません。」という不穏な記述もあって、場合によっては影響するのかな?という気がしています。

GCクラスのソースを追ってみましたが、Collectメソッドの中身は、外部定義の(たぶんネイティブの)メソッド呼び出しだけで、詳細はわかりませんでした。

外部定義のメソッドを呼び出す際の引数指定で、
第一引数 ジェネレーション、第二引数 回収処理のモードの列挙型
を指定するようなのですが、第一引数は全ジェネレーションを指定してあり、
第二引数はDefaultが指定してありました。
回収処理のモードはDefault、Forced、Optimizedの3つがあり、
Defaultの意味はわかりませんが、Forcedが完全な強制だとすると、
Defaultはそうとも限らないのかもしれないなと思った次第です。

推測の域を出ない話ですみません。
環境にも依存しそうな内容なので、Compact Frameworkを使って
実験してみないとわかりませんね。(私には環境がないけれど)


[ メッセージ編集済み 編集者: よねKEN 編集日時 2008-10-02 10:09 ]
なちゃ
ぬし
会議室デビュー日: 2003/06/11
投稿数: 872
投稿日時: 2008-10-02 10:47
Compact Framework のGCは世代別じゃないそうですよ、たしか。
詳細は見てませんが。
momomo
会議室デビュー日: 2006/01/12
投稿数: 6
投稿日時: 2008-10-02 14:40
みなさま、返信と有効な情報ありがとうございます。
いろいろと実験を繰り返して返信遅くなりました。

引用:

GC.Collect が実行されるときには fm2 は参照されています。従って、1つは残ります。

で、ループになっているところには、気づいていませんでした。
GC.Collect() は、全てのジェネレーションに対してコレクトを行いますから、1つは残りますが、全てが残ったままとなるというのは、おかしいです。
ただ、GC が管理するものと IDisposable で管理を任されるものは異なりますから、Dispose を入れたから挙動が変わるというのは違うと思います。



元々、
protected override void Dispose(bool disposing)
内に各コントロールのDispose()を入れることに関してご意見をいただきたかったのですが、それ以前に、Collect()の動作にも気を使わなければいけないということのようですね。測定をしてもその測定方法に問題があれば結果も変わって見える、と。このあたり、ご指摘いただいて初めて気付きました。

そういうことで、まだ測定方法が不完全ですが、実験結果を掲載してみます。
実際にはもっと多くのパターンを試していますが、代表的なものだけ。

◇環境
 .NET Compact Framework 2.0
 Windows Mobile 5.0 Pocket PC エミュレータ(実機と動作が異なっていたら悲しい)

■実験1
 タイトル:Form以外のIDisposableなクラスの大量作成

 手順1:
  ・「GC.GetTotalMemory(false)」して値を取得。
 手順2:
  ・以下の処理を100回繰り返す。
  ・SqlCeConnection、SqlCeCommand、SqlCeDataAdapterおよびFills()を利用
   してSDFファイルから10000レコードSELECTする。
  ・それぞれのnewは敢えてループの中で行う。
  ・Dispose()やnull設定などは一切行わない。
  ・完了した時点で「GC.GetTotalMemory(false)」して値を取得。
 手順3:
  ・「GC.Collect()」を実行。
  ・「GC.GetTotalMemory(false)」して値を取得。

 結果:
  上記手順を3回連続繰り返して、
  手順1→手順2で値増(3MB程度)
  手順2→手順3で値減(手順3=手順1)
  手順1→手順2で値増(3MB程度)
  手順2→手順3で値減(手順3=手順1)。。。
  つまり、Collect()を実行すると値は完全に元に戻り、繰り返しても値は増えない。

 考察:
  少なくとも、この場合、マネージなメモリについては、使用後気にしなくてもよさそう。
  GC.GetTotalMemory(false)ではアンマネージなメモリについては確認
  できていないため、追加実験が必要。

■実験2
 タイトル:Formの大量作成(Formインスタンスにnull設定なし)

 手順1:
  ・「GC.GetTotalMemory(false)」して値を取得。
 手順2:
  ・以下の処理を100回繰り返す。
  ・別Form(Form3)をShowDialog()
   Form3はTimerで自身をClose()
   Form3はComboBoxを120個ほど保持。
   Dispose(bool disposing)内でControls[i].Dispose()を全コントロール分実行。
  ・Form3のDispose()を実行
 手順3:
  ・「GC.Collect()」を実行。
  ・「GC.GetTotalMemory(false)」して値を取得。

 結果:
  上記手順を3回連続して繰り返して
  手順1→手順2で値増(5MB程度)
  手順2→手順3で値減(1MB程度)
  手順1→手順2で値増(5MB程度)
  手順2→手順3で値減(1MB程度)。。。
  つまり、やればやるほど値が増えていく。

 考察:
  マネージなメモリについては実験1とは動作が異なる。
  やっぱりアンマネージなメモリについては未確認。追加実験が必要。

■実験3
 タイトル:Formの大量作成(Formインスタンスにnull設定あり)

 手順1:
  ・「GC.GetTotalMemory(false)」して値を取得。
 手順2:
  ・以下の処理を100回繰り返す。
  ・別Form(Form3)をShowDialog()
   Form3はTimerで自身をClose()
   Form3はComboBoxを120個ほど保持。
   Dispose(bool disposing)内でControls[i].Dispose()を全コントロール分実行。
  ・Form3のDispose()を実行
  ・Form3のインスタンスにnull設定
 手順3:
  ・「GC.Collect()」を実行。
  ・「GC.GetTotalMemory(false)」して値を取得。

 結果:
  上記手順を3回連続して繰り返して
  手順1→手順2で値増(5MB程度)
  手順2→手順3で値減(1MB程度)
  手順1→手順2で値増(5MB程度)
  手順2→手順3で値減(1MB程度)。。。
  つまり、やればやるほど値が増えていく。
  かつ、実験2と同じような結果になった。

 考察:
  実験2と3で結果が変わらなかったため、
  よしもさんとMS有償サポートが仰っているメモリリークは別の方法で
  確認が必要かも。

■実験4
 タイトル:実験3の.NET Framework版

 手順1:
  ・「GC.GetTotalMemory(false)」して値を取得。
 手順2:
  ・以下の処理を100回繰り返す。
  ・別Form(Form3)をShowDialog()
   Form3はTimerで自身をClose()
   Form3はComboBoxを120個ほど保持。
   Dispose(bool disposing)内でControls[i].Dispose()を全コントロール分実行。
  ・Form3のDispose()を実行
  ・Form3のインスタンスにnull設定
 手順3:
  ・「GC.Collect()」を実行。
  ・「GC.GetTotalMemory(false)」して値を取得。

 結果:
  上記手順を3回連続して繰り返して
  手順1→手順2で値増(1MB程度)
  手順2→手順3で値減(1MB程度)
  手順1→手順2で値増(1MB程度)
  手順2→手順3で値減(1MB程度)。。。
  実験3と異なるのは、値が増加し続けることがないこと。

 考察:
  .NET FrameworkとCompactとではマネージなメモリ使用状況の動きが異なる。

◇まとめ
Formとそれ以外(今回の場合DataTableなど)ではやっぱり動作が異なる。
.NET FrameworkとCompactとではマネージなメモリ使用状況の動きが異なる。
null設定の効果は未確認。
ちなみに、実験3を延々繰り返していくと、OutOfMemoryが発生するのを確認することは
できませんでしたが、アプリケーションの動作がどんどん遅くなり、
最終的にはアプリケーションが固まった(かのように見えた)という結果になりました。

引用:

あと、こんな現象があるそうです。
Compact Framework のメモリ管理<live.com>(宇宙仮面のC#研究室)

キャッシュされるのでメモリ使用量が増えていくというのはわかるのですが、それが OutOfMemory を引き起こすまでとなると、確かにおかしいですね。



メモリのキャッシュというのは初耳でした。
実験4が3と異なる結果を得たことで、
実験2と3で増加していくのはこれが原因かもしれません。
それで、たしかに仰る通りメモリ使用量が増えていくのはいいとして、OutOfMemoryまで起きるのが謎です。
ちなみに、この実験を開始してからOutOfMemoryが発生したのは確認できていません。
(最終的に固まる(固まったようにみえる)状況で個人的な時間切れで実験を終えています)

また、宇宙仮面さんのページ内のリンク
http://msdn.microsoft.com/ja-jp/library/s6x0c3a4(VS.80).aspx
によると、
引用:

.NET Compact Framework では、メモリ不足になると、現在実行しているコードに必要のない内部データ構造を積極的に解放します。そのため、メモリが不足した状態になってもプログラムを継続できます。使用できるメモリが少なく、アプリケーションに必要なメモリが不足している場合、.NET Compact Framework はアプリケーションを完全に閉じて、基底にあるすべてのリソースを解放します。メモリ不足によって .NET Compact Framework 自体に障害が発生することはありません。


だそうです。
これって、メモリが不足気味になったらFrameworkがプロセスを勝手に殺す、ということのようにも見えます。
ひょっとしてこの状況でOutOfMemoryが起きているのだろうか?
そうでないとすると、逆に考えてOutOfMemoryが発生する状況というのはどういう状況なのか?
あ、アンマネージなメモリのことをまだ見ていないのでそっちが影響してるかも。
すみません、自問自答です。

Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2008-10-03 14:58
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=305534
これっっぽい。
IContainer オブジェクトは、Components プロパティに配置されているコンポーネントのオブジェクトを保持しなければならないのですが、Compact Framework ではそうなっていない。・・・らしいです。
そりゃ、自力で Dispose を書かなきゃだわ。

回避策として、「自分で Components に追加しよう」というのがあがっています。

スキルアップ/キャリアアップ(JOB@IT)