先月の仕事のお話。
開発中のアプリケーションである一連の操作を3時間近く連続実行すると突然落ちるという現象に遭遇した。HeapやPrivate Bytesといったメモリ量が増えていないことから、リークしているようにも見えず。Desktop Heap、ハンドル数、GDIオブジェクトでも異常値は見られなかった。
結局解決するまでに2か月近くかかったのだが、何とか問題を特定することができました( ´ー`)フゥー...
そして、同じようなことで困った人がいるかも?しれないので、ここにまとめておきます。
ーーーーーーーーーー
.NET FrameworkのSystem.Windows.Forms.Clipboardクラスである条件化においてGetDataメソッドをコールすると、コールするたびに8KBメモリリークします。この状態でGetDataをひたすら繰り返すと、メモリの断片化により2GBのメモリが食いつぶされ予期しない例外が発生します。
発生させるための簡単なコードは以下のようなもの。
// クリップボードにデータをセット DataSet ds = new DataSet(); Clipboard.SetDataObject(new DataObject("TEST", ds)); int count = 1; // クリップボードからデータをゲット while (count < 100000) { if (Clipboard.GetData("TEST") == null) { break; } count++; }
上記の処理を行うとcountが65500回を超えたあたりでGetDataの値が「null」になるため止まります。メモリリークが発生している様子はパフォーマンスモニタでPrivate Bytesの値の増加で確認できます。(原因はAPIの内部の話でMSもバグと認識していないので、不明です)
この状態ではGetDataをした際に「null」が返ってくるのですが、内部的にはOutOfMemoryが返ってきており、さらに別のFormなどをShowしたりするとWin32Exceptionが発生します。
この現象が発生する条件は以下のとおり。
- SetDataObjectでプライベートな型を指定
- SetDataObjectで引数が1つのものもしくは第2引数で「False」を指定
で、当面の回避策としては、SetDataObjectの第2引数に「False」を設定するです。(アプリ終了時にクリップボードの内容を削除したい場合は・・・どうしましょうかね)
尚、Reflector for .NET(http://www.aisto.com/roeder/dotnet/)でSetDataObjectを見てみると、以下のような処理を行っていることがわかります。
public static void SetDataObject(object data) { SetDataObject(data, false); }
えぇ、「False」がデフォルトで指定されています。では、別のメソッドであるSetDataはどうでしょう?
public static void SetData(string format, object data) { IDataObject obj2 = new DataObject(); obj2.SetData(format, data); SetDataObject(obj2, true); }
こっちは「True」です。。。こちらを使っていれば気がつかなかったでしょう。
では、trueとfalseで何が違うか?SetDataObjectを辿っていくと・・・以下のAPIを呼ぶか呼ばないかの違いでしかないようです。
[DllImport("ole32.dll", CharSet=CharSet.Auto, ExactSpelling=true)] public static extern int OleFlushClipboard();
実際にSetDataObjectの第2引数を「False」で上記のAPIを直接コールすれば、当然ながらメモリリークは発生しません。ole32.dllに問題があるので、.NET Framework 3.5でも当然ながら影響を受けます。これ以上のことはわかりませんが、Clipboardクラスを使用していてアプリが突然落ちるといった問題に遭遇している場合は、本対策で解決?(対処療法ですからね・・・つーか、コッソリ修正しといてね>MS)できるかもしれません。
コメント