[Mastering VBScript] メモリリークの罠:大規模Dictionary操作における `RemoveAll` と `Nothing` の決定的な違い
業務自動化の現場において、VBScript(WSH)は「手軽で強力な武器」だ。しかし、数万件、数十万件という膨大なデータを扱うプロダクション環境に持ち込んだ途端、この武器は「メモリを食いつぶす凶器」へと変貌する。
「最初はスムーズに動いていたのに、数時間経つとスクリプトが低速化し、最終的にメモリ不足(Out of Memory)で落ちる」
もし君がこのような現象に直面しているなら、それはコードのロジックが間違っているのではない。オブジェクトのライフサイクル、すなわち「メモリの解放」に対する設計思想が欠落しているのだ。
本稿では、VBScriptにおけるメモリ管理の要である `Scripting.Dictionary` を題材に、プロフェッショナルが実践すべき `RemoveAll` と `Nothing` の使い分け、そして堅牢なリソース管理術を伝授する。
1. なぜメモリは「右肩上がり」に増え続けるのか?
VBScriptは、COM(Component Object Model)という技術の上に成り立っている。オブジェクトの寿命は、そのオブジェクトを参照している「参照カウント(Reference Count)」によって制御される。
多くの開発者が犯す過ちは、「中身を空にしたつもり」で、メモリの「器」を放置することだ。
致命的なアンチパターン:ループ内での再生成
以下のコードを見てほしい。これは一見、論理的に正しいように見えるが、最悪の設計だ。
' --- 悪質な例:メモリリークを誘発する設計 ---
Dim dict, i
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To 100000
' ループのたびに新しいオブジェクトを作成している
' 前のオブジェクトへの参照が適切に切断されていない場合、メモリは解放されない
Set dict = CreateObject("Scripting.Dictionary")
dict.Add i, "Data_" & i
' 何らかの重い処理...
' ここで Set dict = Nothing を忘れる、あるいは
' 単に新しいオブジェクトを代入するだけでは、COMの解放が追いつかないことがある
Next
この書き方では、ループのたびに新しいメモリ領域が確保され、古いオブジェクトが「解放待ち」の状態で堆積していく。これがメモリ消費が右肩上がりに増える正体だ。
2. `RemoveAll` と `Nothing` の真実:使い分けの極意
ここで、我々アーキテクトが使い分けるべき二つの手法を整理する。
`RemoveAll`:器を再利用する(Reuse)
`dict.RemoveAll` は、Dictionaryオブジェクトの「中身(KeyとItem)」をすべて削除するが、オブジェクトそのものはメモリ上に保持し続ける。
- メリット: オブジェクトの生成・破棄のオーバーヘッド(CPU負荷)を抑えられる。
- ユースケース: 同じ構造のデータを、一定のチャンク(塊)ごとに処理する場合。
- 注意点: Dictionaryが保持していた内部的なハッシュテーブルのサイズ(キャパシティ)は、`RemoveAll` を実行しても縮小されない場合がある。つまり、一度巨大になったDictionaryは、中身を空にしても「メモリを大量に確保したままの空箱」として居座り続ける。
`Nothing`:器ごと破壊する(Destroy)
`Set dict = Nothing` は、変数とオブジェクトの紐付けを断ち切る。参照カウントがゼロになれば、COMによってオブジェクトそのものがメモリから抹消される。
- メリット: メモリ領域を完全にOSに返却できる。
- ユースケース: 処理のフェーズが切り替わる時、または巨大なデータを扱い終えた直後。
- 注意点: 再利用するには `CreateObject` を再度呼び出す必要があり、頻繁に行うとパフォーマンスに影響する。
3. プロダクション・クオリティの設計指針
堅牢なツールを作るなら、以下の「ハイブリッド戦略」を採用せよ。
1. 小規模・高頻度の繰り返し $\rightarrow$ `RemoveAll` でオブジェクトを使い回し、計算コストを下げる。
2. 大規模フェーズの終了 $\rightarrow$ `Nothing` で確実にメモリを解放し、次のフェーズに備える。
以下に、実務でそのまま使える、メモリ管理を徹底した実装例を示す。
【実践例】巨大なデータセットを分割処理する堅牢なスクリプト
' ==========================================================================
' Script Name: RobustDataProcessor.vbs
' Description: 巨大なデータをチャンクごとに処理し、メモリリークを徹底排除する
' ==========================================================================
Option Explicit
' 定数定義
Const CHUNK_SIZE = 10000 ' 1万件ごとにメモリをクリーンアップ
Dim totalRecords, i, currentChunkCount
totalRecords = 100000 ' シミュレーション用の総件数
Call Main()
Sub Main()
Dim dict
Dim i
' 1. Dictionaryの初期化
Set dict = CreateObject("Scripting.Dictionary")
On Error Resume Next ' エラーハンドリングの準備
For i = 1 To totalRecords
' データの投入(シミュレーション)
dict.Add i, "Value_" & i
currentChunkCount = currentChunkCount + 1
' 2. チャンク処理のロジック
If currentChunkCount >= CHUNK_SIZE Then
Call ProcessChunk(dict)
' --- メモリ管理の要 ---
' 中身をクリアしてオブジェクトを再利用(CPU負荷軽減)
dict.RemoveAll
currentChunkCount = 0
' ----------------------
End If
Next
' 残りのデータを処理
If dict.Count > 0 Then
Call ProcessChunk(dict)
End If
' 3. 全工程終了後の完全解放
' プログラムの終端では必ず Nothing を明示する
Set dict = Nothing
If Err.Number <> 0 Then
WScript.Echo "Error occurred: " & Err.Description
Else
WScript.Echo "Process completed successfully."
End If
On Error GoTo 0
End Sub
' --------------------------------------------------------------------------
' Sub ProcessChunk(ByRef targetDict)
' 引数: targetDict - 処理対象のDictionaryオブジェクト
' --------------------------------------------------------------------------
Sub ProcessChunk(ByRef targetDict)
' ここにDB書き込みやファイル出力などの重い処理を記述する
' 引数は ByRef で渡し、オブジェクトの生成コストを避ける
' 例: WScript.Echo targetDict.Count & " items processed."
' 処理中にエラーが起きても、呼び出し元で制御できるよう、
' ここでは極力重いオブジェクト(ADODB等)をローカル変数として適切に扱うこと
End Sub
4. アーキテクトからの最終アドバイス
Dictionary 以外のオブジェクトについても、同じ哲学を適用せよ。特に ADODB.Recordset や ADODB.Connection は、メモリリークの温床だ。
- Recordsetは使い終わったら即座に `Close` し、`Nothing` せよ。 `Close` だけではメモリが解放されないケースがある。
- ファイルシステムオブジェクト (FSO) も同様だ。 テキストストリームを開いたら、必ず `Close` してから `Nothing` にする。
まとめ:君が守るべき鉄則
1. ループ内で `CreateObject` を繰り返すな。
2. 再利用するなら `RemoveAll`、捨てるなら `Nothing`。
3. 巨大な塊を扱った後は、迷わず `Nothing` でメモリを返却せよ。
これらを徹底するだけで、君が書くスクリプトの信頼性は、並のエンジニアとは一線を画すものになる。メモリを支配する者が、自動化を支配するのだ。
