検証の動機
Qiitaでこういう記事を書きました。COMオブジェクトであるMicrosoft.Office.Interop.Word
を使用した.NETアプリを作成した、というものです。
昔から知られていることですが、.NETからCOMオブジェクトの呼び出しはラッパーを仲介するため、処理のオーバーヘッドが高くなります。マネージドなライブラリがあるなら、そちらを用いたほうが効率が良いです。
ですが、どうしてもCOMの使用が必要な場面もあります。その場合の、メモリ開放処理をどう書けばちゃんと解放してくれるのか?を調べてみようと思いました。
一応、用語説明
マネージド
MS以外のベンダー(Oracleとか)では「管理対象」と訳している場合があります(反対語は「非管理対象」)。
マネージドコードと言う場合はCLRによって管理されるコードのことを指し、メモリ自動管理の対象にもなります。反対は「アンマネージド」で、C/C++などで記述されたコードがこれに当たる。COMオブジェクトもアンマネージドなコードで記述されたライブラリ。
COMオブジェクト
Component Object Modelの略ですが、昔のVB6.0の時代ではよく使われていたもので、ソフトウェアの再利用が可能になるもの。今回の場合で言えば、WordのAPIがこのCOMオブジェクトに当たります。CLRのメモリ自動管理の対象になりませんので、開放処理を書いてやる必要があります。
コード
本サイトのプロフィールのエリアにGitHubのリンクがありますが、一応、ソリューション全体のソースは以下です。
メモリ開放が必要なのは、以下の「変換」ボタン押下時の、WordファイルからPDFへの変換処理の箇所です。一番下の、finally
句のところです。
/// <summary>
/// 変換ボタンクリック処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnConvert_Click(object sender, EventArgs e)
{
//using RefWord = Microsoft.Office.Interop.Word;として定義
//WordのCOMオブジェクトをセット
RefWord.Application word = new RefWord.Application();
RefWord.Documents docs = word.Documents;
RefWord.Document doc = null;
//toolCompleteに状態を表示
this.toolComplete.Text = string.Empty;
this.Cursor = Cursors.WaitCursor;
try
{
string[] files = Directory.GetFiles(this.txtInput.Text, "*.doc*");
//ファイル数の分ルール
foreach (string f in files)
{
this.toolComplete.Text = Path.GetFileName(f);
var attribute = File.GetAttributes(f);
var fn = Path.GetFileNameWithoutExtension(f);
//テンポラリの隠しファイルは飛ばす
if ((attribute & FileAttributes.Hidden) == FileAttributes.Hidden)
continue;
//PDF出力
doc = docs.Open(f, ReadOnly:true);
doc.ExportAsFixedFormat(this.txtOutput.Text + "\\" + fn + ".pdf", RefWord.WdExportFormat.wdExportFormatPDF);
doc.Close();
}
this.toolComplete.Text = "完了";
} catch(Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} finally
{
//↓計測用
MessageBox.Show(GC.GetTotalMemory(false).ToString());
//COMオブジェクト開放
if (word != null) word.Quit();
Marshal.ReleaseComObject(doc);
Marshal.ReleaseComObject(docs);
Marshal.ReleaseComObject(word);
MessageBox.Show(GC.GetTotalMemory(false).ToString());
//↑計測用
}
this.Cursor = Cursors.Default;
}
GC.GetTotalMemory
は割り当てられているメモリのバイト数を返します。
補足ですが、今回の検証環境は、
- Windows 10 Pro
- .NET Framework 4.6.1
- Office 2019 Word
で実施しました。
結果
開放処理のコードを入れない場合
Marshal.ReleaseComObject
だけでGC.Collect()
を入れない場合です。
if (word != null) word.Quit();
Marshal.ReleaseComObject(doc);
Marshal.ReleaseComObject(docs);
Marshal.ReleaseComObject(word);
結果:
前:505,092
後:521,476
増加していますね。まぁ、解放処理がなされていないようです。
最後にGC.Collect()
MSのドキュメントのサンプル通りの書き方です。
if (word != null) word.Quit();
Marshal.ReleaseComObject(doc);
Marshal.ReleaseComObject(docs);
Marshal.ReleaseComObject(word);
GC.Collect();
結果:
前:505,092
後:211,684
ちゃんと開放してくれているようですね。
前後にGC.Collect()
開放処理の前後だと差はあるのでしょうか。
GC.Collect();
if (word != null) word.Quit();
Marshal.ReleaseComObject(doc);
Marshal.ReleaseComObject(docs);
Marshal.ReleaseComObject(word);
GC.Collect();
結果:
前:505,092
後:196,044
後だけの時よりも、占有領域が減っているようです。
最後にGC.Collect()とGC.WaitForPendingFinalizers()
これを試した理由は、この記事を見たからです。
上の記事の例ではベストプラクティスとして、
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
を開放の前後に入れていますが、今回は後だけの場合と前後の場合も検証してみます。
まずは、後だけのパターン。
if (word != null) word.Quit();
Marshal.ReleaseComObject(doc);
Marshal.ReleaseComObject(docs);
Marshal.ReleaseComObject(word);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
結果:
前:505,092
後:195,104
GC.Collect()
だけの時より減少していますね。
前後にGC.Collect()とGC.WaitForPendingFinalizers()
ベストプラクティスとして挙げられていた書き方。
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect
if (word != null) word.Quit();
Marshal.ReleaseComObject(doc);
Marshal.ReleaseComObject(docs);
Marshal.ReleaseComObject(word);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
結果:
前:505,092
後:195,104
後だけの場合と変化はありません。
結論
COMオブジェクト開放の際は、Marshal.ReleaseComObject(word)
だけでなく、ガーベジコレクションを強制的に走らせる処理を行う必要がある、ということになりますね。
WaitForPendingFinalizers
については、以下を参照。
しかし、前後にガベコレを強制させるコードは、かなり可読性がよくないな…。
以上です。