WSHやHTAでJScriptを使用してDBアクセスやExcelの操作を行なっている際に、開放漏れと思われる現象が発生しました。※2つはまったく別の時期・別のプログラムに発生していたので関連性があるとは思っていなかったのですが、同じ原因でした。
現象
DBアクセス時は処理終了時に「con.Close();」としていました。エクセルアクセス時は処理終了時に「excel.Workbooks.close();」としていました。
その結果以下の現象がおきました。
- DBアクセス時にセッションが残る(OracleのV$SESSIONを確認)
- Excelアクセス時にEXCEL.EXEプロセスが残る(タスクマネージャのプロセス表示で確認)
HTAではHTAアプリを閉じたときやF5キーで再読込を行なったときには開放されます。WSHでは正常終了した場合にはきちんと開放されるのですが、エラー終了した場合には残り続けます。そのためとても厄介です。
DBアクセス時は「con = null;」「con = undefined;」「delete con」などを追記してみました。Excelアクセス時は「excel.Quit();」「excel = null;」を追記してみました。
しかし、(少なくともそれだけでは)効果は全くありませんでした…。
原因
半年以上放置していたのですが、ふと思い出して検索するとすぐに答えが出てきました。Microsoftのサポートサイトにバグとして報告されていました。
原因の欄にはこう書いてありました。
JScript で Excel への参照が保持されています。Quit コマンドが発行されたときに Excel への参照が存在するため、Excel はシャットダウンされません。JScript はガベージ コレクションを行う言語です。つまり、このエンジンでは、変数が NULL に設定された時点ではなく、特定の時点で自身のクリーンアップが行われます。ユーザーが Internet Explorer を終了するか別のページに移動すると、このエンジンは破棄されます。この動作により、ガベージ コレクションが強制的に実行され、Excel への参照が解放されます。
まるでそれが仕様であるかのようにも読めます…。GCされるまでオブジェクトが破棄されないというのは仕様だと思います。しかし、closeメソッドやQuitメソッドを呼び出しても、オブジェクトからExcelやDBへの接続が残るという点は明らかにバグでしょう。
個人的にはこう書くべきではないかと思います。
Quit コマンドが発行されたときに 本来なら Excel への参照を破棄すべきです。しかし参照を保持し続ける為、Excel はシャットダウンされません。
検索結果にはExcelの情報しか出てきませんでしたが、DBアクセス時も同じ原因(のはず)です。実際、同じ方法で解消できました。単にJScriptでDBアクセスする人が少ないというだけのことだと思います。また、JScriptのGCの基本的な仕組みの問題のようですので、すべてのオブジェクトで発生する可能性のある問題だと思います。
対応策
上記のサイトの記述を参考にし、クローズ後にCollectGarbageを実行するようにしたところ、DBセッション・Excelともにきちんと開放されることを確認できました。
Excelへのアクセス・関数内ローカル変数の場合、null設定不要・setTimeout必要
function …() {
var excel = new ActiveXObject("Excel.Application");
excel.Workbooks.open(EXCEL_FILE);
// …処理
excel.Workbooks.close();
setTimeout(CollectGarbage, 1);
}
DBアクセス・関数内ローカル変数の場合、null設定不要・setTimeout必要
function …() {
var con = new ActiveXObject("ADODB.Connection");
con.open(接続文字列);
// …処理
con.close();
setTimeout(CollectGarbage, 1);
}
DBアクセス・グローバル変数の場合(open・close処理が分離されている)、null/undefined設定必要・setTimeout不要
var con;
function dbOpen() {
var con = new ActiveXObject("ADODB.Connection");
con.open(接続文字列);
}
function …() {
// …処理
}
function dbClose() {
con.close();
con = null;
CollectGarbage();
}
注目していただきたいのはグローバル変数の場合です。変数が関数内で宣言されている場合には、関数の終了と同時に変数が使用されなくなるため、nullの代入は必要ありません。(必要ないけど書いちゃう人はいますけど…)
しかし、グローバル変数として宣言されている場合には、リロードやブラウザを閉じでもしない限りは、明示的な参照の切断つまりnullの代入が必要になります。
そしてCollectGarbageの実行タイミング。グローバル変数の場合には即時実行で問題ありませんが、ローカル変数の場合には、タイミングを少しずらす必要があるようです。理由はよくわかりませんでしたが…。
余談
いままでJavaScriptでは「変数は使わなくなったら開放される」「あえてnullを代入することは意味は無い」と思っていました。確かに関数の最後でnullを代入することは意味が無いのですが、グローバル変数にしている場合には、意味がある場合があるということがわかりました。
ちなみにこのようなリークはVBScriptでは発生しません。
2011/12/01