トップ回答者
DrawToBitmapのメモリリーク

質問
-
例えばピクチャーボックスがあったとして、それを取り込むときに
ループ内で DrawToBitmap を実行すると、メモリを消費し続けて強制終了します。
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
}
解決策があるでしょうか?それとも根本的に何かを見落としているのでしょうか?
回答
-
>メモリが足りなくなってエラーが発生する前には行われると思っていましたので、違和感を感じます。
.NET のガベージコレクションは「メモリが足りなそうになったことを検知して未参照のメモリを回収する」という仕様ではありません。
また、Bitmap クラスの場合、.NET が管理するヒープメモリにどれだけ余裕があっても、Bitmap クラスがカプセルしているアンマネージリソースが不足していたら、クラスインスタンスの生成に失敗することは十分にありえます。(その失敗をどういう例外でプログラマにレポートするかは、そのクラスの実装依存です)
- 回答としてマーク ton-chan 2010年12月19日 4:33
-
ガベージ コレクションの基礎 を読んできたのでちょっと補足。
スワップが発生するような本当にメモリが足りないときには.NETのガーベージコレクタは自動的に動作します。
ただし今回のような、メモリは余っているだろうけどBitmapクラスが扱うアンマネージリソースが不足しているかどうかは検知できない&検知できたとして何を解放すればBitmapクラスの扱うアンマネージリソースが増加するのかわからないため、自動的に動作するわけではありません。ガーベージコレクタにはワークステーションとサーバーの2種類があり、通常はワークステーションが使われます(mscorwks.dllとmscorsvc.dllのこと?)。この場合、ユーザースレッドで動作するので、今回のような小さなループ内ではガーベージコレクタが起動しないものと思われます。自動的に起動するのはメソッドのreturnのときとかじゃないかな?
- 回答としてマーク ton-chan 2010年12月19日 4:34
-
これが、MSDNのDrawToBitmapの解説にのっている「サイズの大きなビットマップ 云々」のことかもしれません。
Bitmapクラスが原因と思いPictureBoxは関係ないと思っていました。各種コード生成してみましたがILレベルで目立った違いもなく悩んでいましたが、よくよく見ればこのメソッドはPictureBoxクラスではなく、Control::DrawToBitmap() なのですね。
.NET Frameworkの該当部分のソースコードを確認したところ、DrawToBitmap()メソッドの中で一時的にBitmapクラスインスタンスを作成し、しかも明示的な開放はせず、ガーベージコレクションに任せる実装がされていました。「サイズの大きなビットマップ」でビンゴのようです。
ただ、PictureBoxを使っているのなら
pictureBox1->Image = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
で済むような気もしますが。
- 回答としてマーク ton-chan 2010年12月19日 5:45
-
外池さんありがとうございます。
すみません。一応自己解決しました。while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
GC::Collect();
}
ループ内でガベージコレクションを実行させれば、メモリリークはしませんでした。
メモリリークが発生したのは、
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
}
の場合
メモリリークが発生しないのは
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
GC::Collect();
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
GC::Collect();
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
delete bmp;
}
の場合でした。
結果として、どうもGCが思ったように働いていないような感じがします。
GCはいつ行われるかわからないのはわかってはいましたが、メモリが足りなくなってエラーが発生する前には行われると思っていましたので、違和感を感じます。
これらのことについて何か情報を持っている方がいらっしゃれば教えてください。
- 回答としてマーク ton-chan 2010年12月19日 4:33
-
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { if(timer1.Enabled == false) { timer1.Interval = 1; timer1.Start(); } else timer1.Stop(); } private void timer1_Tick(object sender, EventArgs e) { Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); pictureBox1.DrawToBitmap(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height)); pictureBox2.Image = bmp; } }
C#で記述して恐縮なのですが、pictureBox1はImageプロパティーがデザイン時にセットしてあって、画が表示されています。これをpictureBox2にコピーするプログラムにしてみました。で、timer1で同じ操作を繰り返します。
この場合ですと、pictureBox2に以前にコピーされた古いbmpは明示的にDisposeしていませんが、新しいコピーがImageに代入された時点で、古いコピーへの参照は切れています。そうすると、古いコピーはGCの対象になります。
で、timer1をブンブン回してみたのですが・・・、短期的なメモリー消費量の増加は観測されるものの、適宜GCされて回復します。特に問題ありません。
C++/CLIは、deleteでDisposeできるんですね・・・。だとすれば、最初の方法で大丈夫なはずですよねぇ。
----------追記---------
timer1を使うのではなくて、直接ループして新しいBitmapへのDrawToBitmapを繰り返す方法(ton-chanさんが最初に示されたものと同じ)をC#でやってみましたが、まったく問題ありません。適宜GCされています。たとえ、bmpのDisposeをしなくても問題ありません。参照さえ切れてくれていれば、そのうちちゃんとメモリは回収されるハズ、という仕様どおりの動きになっています。
(ホームページを再開しました)- 回答としてマーク ton-chan 2010年12月19日 4:33
すべての返信
-
外池さんありがとうございます。
すみません。一応自己解決しました。while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
GC::Collect();
}
ループ内でガベージコレクションを実行させれば、メモリリークはしませんでした。
メモリリークが発生したのは、
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
}
の場合
メモリリークが発生しないのは
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
pictureBox1->DrawToBitmap(bmp, pictureBox1->ClientRectangle);
delete bmp;
GC::Collect();
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
GC::Collect();
}
while(1) {
Bitmap^ bmp = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
delete bmp;
}
の場合でした。
結果として、どうもGCが思ったように働いていないような感じがします。
GCはいつ行われるかわからないのはわかってはいましたが、メモリが足りなくなってエラーが発生する前には行われると思っていましたので、違和感を感じます。
これらのことについて何か情報を持っている方がいらっしゃれば教えてください。
- 回答としてマーク ton-chan 2010年12月19日 4:33
-
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { if(timer1.Enabled == false) { timer1.Interval = 1; timer1.Start(); } else timer1.Stop(); } private void timer1_Tick(object sender, EventArgs e) { Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); pictureBox1.DrawToBitmap(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height)); pictureBox2.Image = bmp; } }
C#で記述して恐縮なのですが、pictureBox1はImageプロパティーがデザイン時にセットしてあって、画が表示されています。これをpictureBox2にコピーするプログラムにしてみました。で、timer1で同じ操作を繰り返します。
この場合ですと、pictureBox2に以前にコピーされた古いbmpは明示的にDisposeしていませんが、新しいコピーがImageに代入された時点で、古いコピーへの参照は切れています。そうすると、古いコピーはGCの対象になります。
で、timer1をブンブン回してみたのですが・・・、短期的なメモリー消費量の増加は観測されるものの、適宜GCされて回復します。特に問題ありません。
C++/CLIは、deleteでDisposeできるんですね・・・。だとすれば、最初の方法で大丈夫なはずですよねぇ。
----------追記---------
timer1を使うのではなくて、直接ループして新しいBitmapへのDrawToBitmapを繰り返す方法(ton-chanさんが最初に示されたものと同じ)をC#でやってみましたが、まったく問題ありません。適宜GCされています。たとえ、bmpのDisposeをしなくても問題ありません。参照さえ切れてくれていれば、そのうちちゃんとメモリは回収されるハズ、という仕様どおりの動きになっています。
(ホームページを再開しました)- 回答としてマーク ton-chan 2010年12月19日 4:33
-
>メモリが足りなくなってエラーが発生する前には行われると思っていましたので、違和感を感じます。
.NET のガベージコレクションは「メモリが足りなそうになったことを検知して未参照のメモリを回収する」という仕様ではありません。
また、Bitmap クラスの場合、.NET が管理するヒープメモリにどれだけ余裕があっても、Bitmap クラスがカプセルしているアンマネージリソースが不足していたら、クラスインスタンスの生成に失敗することは十分にありえます。(その失敗をどういう例外でプログラマにレポートするかは、そのクラスの実装依存です)
- 回答としてマーク ton-chan 2010年12月19日 4:33
-
ガベージ コレクションの基礎 を読んできたのでちょっと補足。
スワップが発生するような本当にメモリが足りないときには.NETのガーベージコレクタは自動的に動作します。
ただし今回のような、メモリは余っているだろうけどBitmapクラスが扱うアンマネージリソースが不足しているかどうかは検知できない&検知できたとして何を解放すればBitmapクラスの扱うアンマネージリソースが増加するのかわからないため、自動的に動作するわけではありません。ガーベージコレクタにはワークステーションとサーバーの2種類があり、通常はワークステーションが使われます(mscorwks.dllとmscorsvc.dllのこと?)。この場合、ユーザースレッドで動作するので、今回のような小さなループ内ではガーベージコレクタが起動しないものと思われます。自動的に起動するのはメソッドのreturnのときとかじゃないかな?
- 回答としてマーク ton-chan 2010年12月19日 4:34
-
外池です。
C#で以下のように書いてみて、Windowsアプリで「反応なし」状態でブン回しても、ちゃんとGCはされました。(bmp.Dispoase()の行をコメントアウトしても、メモリー消費が際限なく増えることはありませんでした。)
参考になれば幸いです。
for(; ; ) { Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); pictureBox1.DrawToBitmap(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height)); bmp.Dispose();
(ホームページを再開しました) -
外池さま、佐祐理さま、渋木さま
ありがとうございます。外池さま
提案して頂いたコードを、C++/CLI, C# の両方で確認してみました。
C#ではある程度の間隔で問題なくメモリは開放されていました。
C++/CLIの場合は、やはりメモリが開放されませんでした。
ただ、キャプチャするサイズが小さければメモリは開放されているようで、止まることはありませんでした。
これが、MSDNのDrawToBitmapの解説にのっている「サイズの大きなビットマップ 云々」のことかもしれません。佐祐理さま
Bitmap をスタックライクで宣言しても、結果は同じでした。
渋木さま
このような場合には、GC::Collect() で明示的にガベージコレクションを実行すべきなんでしょうね。 -
これが、MSDNのDrawToBitmapの解説にのっている「サイズの大きなビットマップ 云々」のことかもしれません。
Bitmapクラスが原因と思いPictureBoxは関係ないと思っていました。各種コード生成してみましたがILレベルで目立った違いもなく悩んでいましたが、よくよく見ればこのメソッドはPictureBoxクラスではなく、Control::DrawToBitmap() なのですね。
.NET Frameworkの該当部分のソースコードを確認したところ、DrawToBitmap()メソッドの中で一時的にBitmapクラスインスタンスを作成し、しかも明示的な開放はせず、ガーベージコレクションに任せる実装がされていました。「サイズの大きなビットマップ」でビンゴのようです。
ただ、PictureBoxを使っているのなら
pictureBox1->Image = gcnew Bitmap(pictureBox1->Size.Width, pictureBox1->Size.Height);
で済むような気もしますが。
- 回答としてマーク ton-chan 2010年12月19日 5:45
-
佐祐理さま、ありがとうございました。
自分もソースコードを確認しました。確かにそうなっていますね。
えーと、実際このコードを使ってどうこうってことはしません。
いろいろとやってみたくなる性分で、グラフィクス関係を弄っていたところ、この現象に出くわしました。
Win32APIでやっていたころはそれはそれで面倒でしたが、こういうところはプログラマが自分で管理できたのでそう意味ではAPIはよかったなと感じました。貴重な時間を割いて、いろいろとやって頂きありがとうございます。
ただ、C#とC++/CLIでそれぞれ挙動が違うのどういった理由なのか知りたいなぁと思います。