【入門編】【WMIメモリリーク対策】SWbemServices.ExecQuery 大量取得時におけるリソース圧迫回避と最適化処理 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

こんにちは!VBScriptの世界へようこそ。
マクロの記録や簡単な自動化から一歩進んで、「Windowsのシステム管理をスマートに自動化したい!」と思ったとき、避けて通れないのがWMI(Windows Management Instrumentation)ですね。

「すべてのコンピュータのプロセス一覧を取得したい」
「社内PCのネットワーク設定を一括で調べたい」

そんなとき、`SWbemServices.ExecQuery` メソッドは魔法のような強力なツールです。しかし、この魔法、使い方を誤ると「メモリリーク」というシステムからの強烈なしっぺ返しを食らうことになります。数百台、あるいは数千台の端末を相手にする現場では、これが原因でスクリプトが突然フリーズしたり、サーバーのメモリを食い潰したりする惨事を何度も見てきました。

でも、安心してください。今日お伝えする「極限の知見」をここでマスターすれば、あなたの書くWMIスクリプトは見違えるほど軽快になり、リソースを無駄に食わない「プロのコード」に生まれ変わります。

ここをクリアすれば、VBScriptの基礎と応用力はバッチリですよ。一緒に本質を学んでいきましょう!

1. なぜ、大量のWMIクエリはメモリを圧迫するのか?

まず、敵を知ることから始めましょう。
通常、VBScriptでWMIクエリを投げる時、私たちは次のようなコードを書きます。

Set objWMIServices = GetObject(“winmgmts:\\.\root\cimv2”)
Set colItems = objWMIServices.ExecQuery(“SELECT FROM Win32_Process”)

For Each objItem in colItems
WScript.Echo objItem.Name
Next

一見、何の問題もない美しいコードに見えますよね。しかし、ここにVBScriptとWMIの深い闇(オブジェクトのライフサイクル)が隠されています。

スナップショットという名の「重荷」

デフォルトの状態では、`ExecQuery` を実行すると、OSは「その瞬間におけるすべてのオブジェクトのコピー(スナップショット)」を一気にメモリ上に作成します。
もし対象のクラスが `Win32_Process` や `Win32_NTLogEvent` のように、数千・数万件もある巨大なデータだったらどうなるでしょうか?

1. メモリ上に巨大なコレクションがまるごと展開される。
2. VBScriptの裏で動いているCOMコンポーネントが、そのすべてのオブジェクトの参照を保持し続ける。
3. ループが終わっても、スクリプトが終了するまでメモリが解放されない(=メモリリーク)。

これが、大規模環境や長時間の常駐スクリプトでメモリが枯渇するメカニズムです。現場のエンジニア泣かせの典型的な罠ですね。

2. 解決策:`wbemFlagForwardOnly` とストリーム処理

この問題を一刀両断するのが、「前方オンリー・フラグ(Forward-Only)」という概念です。

通常のクエリが「全データを一度にトランクに詰め込む旅行」だとすれば、前方オンリーは「目の前に流れてくるベルトコンベアから、一つずつ荷物を受け取る作業」です。

魔法のフラグ:`wbemFlagForwardOnly` (32)

WMIには、クエリの振る舞いを制御する定数がいくつか用意されています。その中でも最も重要なのがこれです。

  • `wbemFlagForwardOnly` (値: 32)
  • 結果セットを「前方に向かってのみ進む(巻き戻し不可)」ストリームとして取得します。
  • メモリ上に全データを保持せず、データを1件取得して処理したら、次のデータを取得するという省メモリなストリーム処理が可能になります。
  • `wbemFlagReturnImmediately` (値: 16)
  • クエリの結果を待たずに、即座に制御をスクリプトに戻します(非同期的な挙動の制御)。

この2つを足し合わせた `16 + 32 = 48` というフラグを `ExecQuery` の第2引数に渡すのが、プロの現場のデファクトスタンダードです。

3. 実践!メモリリークを完全回避する最適化コード

それでは、実際に現場でそのまま使える、極限まで最適化されたVBScriptのコードを見てみましょう。丁寧なコメントを入れているので、開発環境にコピペして動きを確認してみてください。

‘ ==============================================================================
‘ スクリプト名: OptimizedWMIQuery.vbs
‘ 概要: WMI大量データ取得時のメモリリークを回避する最適化サンプル
‘ ==============================================================================

Option Explicit

Sub Main()
Dim objSWbemLocator, objWMIServices, colItems, objItem
Dim queryStr

‘ 定数定義(WMIフラグ)
‘ wbemFlagReturnImmediately (16) + wbemFlagForwardOnly (32) = 48
Const WMI_FORWARD_ONLY = 48

WScript.Echo “— WMI最適化クエリの実行を開始します —”

On Error Resume Next

‘ 1. コネクションの確立(Locatorを使用すると接続エラーをハンドリングしやすい)
Set objSWbemLocator = CreateObject(“WbemScripting.SWbemLocator”)
Set objWMIServices = objSWbemLocator.ConnectServer(“.”, “root\cimv2”)

If Err.Number <> 0 Then
WScript.Echo “[エラー] WMIへの接続に失敗しました: ” & Err.Description
Exit Sub
End If

‘ 2. 対象クエリの定義(例としてプロセス一覧を取得)
queryStr = “SELECT Name, ProcessId, WorkingSetSize FROM Win32_Process”

‘ 3. 【最重要】フラグを指定してExecQueryを実行
‘ ここで 48 を指定することで、前方オンリーの軽量ストリームを取得する
Set colItems = objWMIServices.ExecQuery(queryStr, , WMI_FORWARD_ONLY)

If Err.Number <> 0 Then
WScript.Echo “[エラー] クエリの実行に失敗しました: ” & Err.Description
Exit Sub
End If

On Error Goto 0

‘ 4. ストリームからのデータ処理
Dim count
count = 0

For Each objItem In colItems
‘ ForwardOnlyの制約上、ここでの「objItem」は使い捨てのストリーム要素となる
count = count + 1

‘ 動作確認として最初の10件だけコンソールに出力
If count <= 10 Then WScript.Echo "プロセス名: " & objItem.Name & " (PID: " & objItem.ProcessId & ")" End If ' 【重要】ループ内で取得した個別オブジェクトも、不要になったら明示的にNothingを入れる意識を持つ Set objItem = Nothing Next WScript.Echo "--- 処理完了 ---" WScript.Echo "総処理プロセス数: " & count ' 5. オブジェクトの明示的解放(メモリのクリーンアップ) Set colItems = Nothing Set objWMIServices = Nothing Set objSWbemLocator = Nothing End Sub ' メイン処理の呼び出し Call Main() ---

4. エンジニアが陥りがちな「やってはいけない」アンチパターン

最後に、現場でよく見かける「メモリリークを引き起こす危険な書き方」をいくつか挙げておきます。これらを避けるだけでも、スクリプトの安定性が劇的に向上します。

1. `Select ` の安易な乱用

  • 必要なプロパティだけを `SELECT Name, ProcessId FROM …` のように絞り込みましょう。全プロパティ(“)を取得すると、不要な内部データまでメモリにロードされます。

2. 前方オンリー (`ForwardOnly`) なのにコレクションの長さを測ろうとする

  • `colItems.Count` を使おうとしていませんか?
  • 前方オンリーのストリームに対して `.Count` を実行すると、エラーになるか、全データを無理やりメモリ上にスキャンし直すため、フラグの意味が完全に失われます。 件数が必要な場合は、ループ内でカウンター変数をインクリメント(`count = count + 1`)する手法を取りましょう。

3. オブジェクトの解放忘れ

  • VBScriptはスクリプト終了時に一応メモリを解放してくれますが、長時間のループや、IISからの呼び出し、バッチ処理で何個もスクリプトを連続キックする環境では致命傷になります。使い終わった変数には `Set xxx = Nothing` を代入する癖をつけましょう。

まとめ

いかがでしたでしょうか?
今日のポイントをギュッと凝縮すると、以下の3点に集約されます。

  • 大量のWMIデータを扱うときは、デフォルトのスナップショット取得がメモリリークの原因になる。
  • `wbemFlagForwardOnly` (フラグ値: 48) を使って、軽量なストリーム処理へ切り替える。
  • 前方オンリーのストリームでは `.Count` が使えないため、カウンターで代用し、最後に `Nothing` で確実に解放する。

この作法さえ守れば、VBScriptは今でもWindows管理において最も頼りになる、軽くて速い最高の相棒です。
「動けばいいや」のコードから、「リソースまで美しく管理できる」プロのエンジニアへ。今日の知見をぜひあなたの開発現場で活かしてくださいね!

タイトルとURLをコピーしました