Visio VBAがメモリを食いつぶす真の理由
エンタープライズシステムにおいて、数千から数万ものシェイプを含む図面を自動生成・解析するVisio VBAスクリプトは、しばしば深刻なメモリ肥大化と、最終的なアプリケーションの異常終了(クラッシュ)という現実に直面します。ExcelやWordといった他のOffice製品と比較して、Visioのメモリ管理が圧倒的に過酷である理由は、その内部アーキテクチャ(オブジェクトグラフの複雑性)にあります。
Visioの各シェイプ(Shape)は、単なる描画オブジェクトではありません。それぞれが「シェイプシート(ShapeSheet)」と呼ばれる巨大なスプレッドシートを内包しており、ジオメトリ、接続情報、カスタムプロパティ、ユーザー定義セルなどが相互に依存し合っています。これらはCOM(Component Object Model)レイヤーを介してVBAに公開されますが、VBA開発者が何気なく書くコードが、このCOMの参照カウント(Reference Counting)を歪め、メモリ上に「解放不可能なゾンビオブジェクト」を大量に生み出しているのです。
本稿では、Visio VBAにおけるオブジェクトのライフサイクルを完全に掌握し、数万回のループ処理でもメモリ消費量をフラットに維持するための極限のコーディング規約と、メモリ監視付きの実践コードを提示します。
—
COM参照カウントの罠と「匿名一時オブジェクト」の発生
VBAは、COMのルールに従ってオブジェクトの寿命を「参照カウント」で管理しています。オブジェクトへの参照が作成されるとカウントが`+1`され、変数がスコープを外れるか、明示的に`Set Object = Nothing`されると`-1`になります。カウントが`0`になった時点で、初めてメモリから解放されます。
しかし、以下のようなコードは、内部で「匿名一時オブジェクト(Anonymous Temporary Objects)」を生成し、メモリリークの温床となります。
‘ 破滅への一歩:ドット連結による暗黙の参照
Dim val As Double
val = ActivePage.Shapes(1).Cells(“Width”).ResultIU
この1行において、VBAおよびVisioの内部では以下の処理が行われています。
1. `ActivePage` から `Shapes` コレクションを取得(中間オブジェクト1)
2. `Shapes` から `Item(1)`(Shapeオブジェクト)を取得(中間オブジェクト2)
3. `Shape` から `Cells(“Width”)`(Cellオブジェクト)を取得(中間オブジェクト3)
4. `Cell.ResultIU` を評価して `val` に代入
このとき、中間オブジェクト1〜3は変数に代入されていないため、VBAからは明示的に解放(`Nothing`代入)できません。通常、これらはプロシージャ終了時に解放されるはずですが、Visioの複雑な内部イベント(再計算チェーンなど)と絡み合うことで、参照カウントが正しくゼロに戻らず、メモリ空間に残留し続ける現象が多発します。
特に数千回のループ内でこの記述を行うと、プロセス(`Visio.exe`)のプライベートコミット電荷が単調増加し、最終的に「Out of Memory(メモリ不足)」で沈黙します。
—
メモリリークを撲滅する「3つの鉄則」
Visioのメモリリークを完全に防ぎ、数時間に及ぶバッチ処理を安定稼働させるための鉄則は以下の3点に集約されます。
1. 「1変数・1参照」の原則(ドット連結の禁止)
すべてのCOMオブジェクトは、個別の一時変数に格納してアクセスします。中間オブジェクトをブラックボックスにしてはなりません。
2. 逆順解放(LIFO: Last-In, First-Out)の実践
オブジェクトを解放する際は、「生成された順序の逆」で行います。
`Shape` ➔ `Page` ➔ `Document` ➔ `Application` の順に解放することで、親オブジェクトが子オブジェクトの参照を掴み続ける「循環参照」に似たデッドロックを防ぎます。
3. エラーハンドラ内での確実なクリーンアップ
正常系ルートだけでなく、例外(エラー)発生時にも、確実にすべてのオブジェクト変数を順次解放する単一のクリーンアップ経路(`CleanUp` ラベル)を設計します。
—
【実践】メモリプロファイリング機能付き・超高速ループ処理パターン
以下に示すVBAコードは、大量のシェイプを走査・更新するシナリオにおいて、Windows APIを用いてプロセスの物理メモリ使用量(Working Set)をリアルタイムに監視しながら、メモリリークを完全に排除して実行する極限のテンプレートです。
32bit/64bitの双方のOffice環境に対応したAPI宣言(VBA7/Win64対応)を含んでいます。
Option Explicit
‘ ==============================================================================
‘ Windows API 構造体・関数宣言(メモリプロファイリング用)
‘ ==============================================================================
If VBA7 Then
Private Type PROCESS_MEMORY_COUNTERS
cb As Long
PageFaultCount As Long
PeakWorkingSetSize As LongPtr
WorkingSetSize As LongPtr ‘ 物理メモリ使用量(バイト)
QuotaPeakPagedPoolUsage As LongPtr
QuotaPagedPoolUsage As LongPtr
QuotaPeakNonPagedPoolUsage As LongPtr
QuotaNonPagedPoolUsage As LongPtr
PagefileUsage As LongPtr
PeakPagefileUsage As LongPtr
End Type
Private Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Private Declare PtrSafe Function GetProcessMemoryInfo Lib “psapi.dll” ( _
ByVal hProcess As LongPtr, _
ByRef ppsmemCounters As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Else
Private Type PROCESS_MEMORY_COUNTERS
cb As Long
PageFaultCount As Long
PeakWorkingSetSize As Long
WorkingSetSize As Long
QuotaPeakPagedPoolUsage As Long
QuotaPagedPoolUsage As Long
QuotaPeakNonPagedPoolUsage As Long
QuotaNonPagedPoolUsage As Long
PagefileUsage As Long
PeakPagefileUsage As Long
End Type
Private Declare Function GetCurrentProcess Lib “kernel32” () As Long
Private Declare Function GetProcessMemoryInfo Lib “psapi.dll” ( _
ByVal hProcess As Long, _
ByRef ppsmemCounters As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
End If
‘ ==============================================================================
‘ メイン処理:数万個のセルを安全に更新する超高速・省メモリループ
‘ ==============================================================================
Public Sub ExecuteBulkUpdateWithZeroLeak()
‘ Visioオブジェクト変数の宣言(すべて個別の変数として定義)
Dim vsoApp As Visio.Application
Dim vsoDoc As Visio.Document
Dim vsoPages As Visio.Pages
Dim vsoPage As Visio.Page
Dim vsoShapes As Visio.Shapes
Dim vsoShape As Visio.Shape
Dim vsoCellWidth As Visio.Cell
Dim vsoCellHeight As Visio.Cell
Dim i As Long
Dim shapeCount As Long
Dim startMemory As Double
Dim currentMemory As Double
‘ 初期メモリ状態の取得
startMemory = GetMemoryUsageMB()
Debug.Print “— 処理開始時のメモリ: ” & Format(startMemory, “0.00”) & ” MB —”
‘ アプリケーション参照の取得
Set vsoApp = Visio.Application
‘ 1. パフォーマンス最適化(描画と再計算の抑制)
‘ これにより不要な描画用中間オブジェクトの生成を根本から防ぐ
vsoApp.ScreenUpdating = False
vsoApp.DeferRecalc = True
On Error GoTo ErrorHandler
‘ アクティブドキュメントとページの取得
Set vsoDoc = vsoApp.ActiveDocument
If vsoDoc Is Nothing Then
Err.Raise vbObjectError + 1001, , “アクティブなドキュメントが存在しません。”
End If
Set vsoPages = vsoDoc.Pages
Set vsoPage = vsoPages.Item(1) ‘ 1番目のページを対象とする
Set vsoShapes = vsoPage.Shapes
shapeCount = vsoShapes.Count
Debug.Print “走査対象シェイプ数: ” & shapeCount
‘ 2. メモリ効率を最大化するループ処理
For i = 1 To shapeCount
‘ ループ内で取得するオブジェクトは毎回確実に初期化・解放する
Set vsoShape = vsoShapes.Item(i)
‘ 特定のセルに対する参照を個別に取得(ドット連結の回避)
If vsoShape.CellExists(“Width”, Visio.VisExistsFlags.visExistsAnywhere) <> 0 Then
Set vsoCellWidth = vsoShape.Cells(“Width”)
‘ セル値の更新(例:幅を10%拡張)
vsoCellWidth.Result(“in”) = vsoCellWidth.Result(“in”) 1.1
‘ Cellオブジェクトの即時解放
Set vsoCellWidth = Nothing
End If
‘ 高負荷時のメモリスタックを防ぐために、一定周期ごとにNothing化とDoEventsを挟む
If i Mod 500 = 0 Then
‘ Shapeオブジェクトの参照を一旦完全にクリア
Set vsoShape = Nothing
‘ OSに制御を戻し、COMのガベージコレクションを促す
DoEvents
‘ メモリ使用量のログ出力
currentMemory = GetMemoryUsageMB()
Debug.Print “処理件数: ” & i & ” / メモリ: ” & Format(currentMemory, “0.00”) & ” MB”
End If
‘ 個別ループ末尾での確実な解放
If Not vsoShape Is Nothing Then Set vsoShape = Nothing
Next i
Debug.Print “— 走査完了 —”
NormalExit:
‘ 3. 正常系クリーンアップ(LIFO原則に基づく逆順解放)
On Error Resume Next ‘ クリーンアップ中のエラーによる無限ループを防止
‘ パフォーマンス設定の復元
If Not vsoApp Is Nothing Then
vsoApp.ScreenUpdating = True
vsoApp.DeferRecalc = False
End If
‘ 子オブジェクトから親オブジェクトへ向かって段階的にNothingを代入
If Not vsoCellHeight Is Nothing Then Set vsoCellHeight = Nothing
If Not vsoCellWidth Is Nothing Then Set vsoCellWidth = Nothing
If Not vsoShape Is Nothing Then Set vsoShape = Nothing
If Not vsoShapes Is Nothing Then Set vsoShapes = Nothing
If Not vsoPage Is Nothing Then Set vsoPage = Nothing
If Not vsoPages Is Nothing Then Set vsoPages = Nothing
If Not vsoDoc Is Nothing Then Set vsoDoc = Nothing
If Not vsoApp Is Nothing Then Set vsoApp = Nothing
‘ 最終メモリの評価
currentMemory = GetMemoryUsageMB()
Debug.Print “— クリーンアップ後のメモリ: ” & Format(currentMemory, “0.00”) & ” MB (差分: ” & Format(currentMemory – startMemory, “0.00”) & ” MB) —”
Exit Sub
ErrorHandler:
Debug.Print “エラー発生: ” & Err.Number & ” – ” & Err.Description
Resume NormalExit
End Sub
‘ ==============================================================================
‘ ヘルパー関数:自プロセスの物理メモリ使用量(MB)を計測
‘ ==============================================================================
Private Function GetMemoryUsageMB() As Double
Dim hProcess As LongPtr
Dim pmc As PROCESS_MEMORY_COUNTERS
Dim cb As Long
cb = LenB(pmc)
pmc.cb = cb
‘ 自プロセスの擬似ハンドルを取得
hProcess = GetCurrentProcess()
‘ メモリ情報の取得
If GetProcessMemoryInfo(hProcess, pmc, cb) <> 0 Then
‘ バイトからメガバイト(MB)へ変換
GetMemoryUsageMB = CDbl(pmc.WorkingSetSize) / 1024# / 1024#
Else
GetMemoryUsageMB = 0#
End If
End Function
—
徹底解説:なぜこのコードはリークしないのか?
上記コードには、Visioエンジニアが突き当たる「壁」を打破するためのアーキテクチャ設計が施されています。
1. `DoEvents` によるメッセージループの消化
VBAのCOMオブジェクト解放は、スレッドのメッセージキューが消化されるタイミングと同調することがあります。長大な同期ループを回し続けると、VBAが「解放予定」としたメモリが実際にはOSに返却されず蓄積します。`i Mod 500 = 0` のタイミングで `DoEvents` を実行することにより、保留されていたCOMメッセージが処理され、実メモリが解放されます。
2. `ScreenUpdating = False` と `DeferRecalc = True` の相乗効果
Visioにおいて最もメモリを消費する処理の一つが「画面描画の更新」と、それに伴う「シェイプシートセルの相互再計算(Dependency Graphの解決)」です。
- `ScreenUpdating = False` は、描画バッファ(一時ビットマップ等)の不要な再生成を抑制します。
- `DeferRecalc = True` は、セルの値を変更するたびに行われる図面全体の再計算を一時停止し、最後に一括で実行させます。これにより、中間計算プロセスで発生する一時メモリが劇的に削減されます。
3. エラーハンドラをバイパスしない「構造化クリーンアップ」
多くのVBAコードが「途中でエラーが発生した際に、オブジェクトの参照を保持したまま終了する」という致命的なミスを犯します。
上記のコードでは、エラーが発生しても必ず `NormalExit` ラベルを経由し、宣言したオブジェクトのすべてに対して、生成とは逆の順序で `Set = Nothing` が実行されるように制御フローを一本化しています。
—
現場で遭遇する「レガシー・システム」との対峙
社内インフラやレガシーなCAD連携システムでは、いまだに32bit版のVisioが稼働しているケースが珍しくありません。32bitプロセスに許されたユーザー仮想アドレス空間は、実質的に2GBが上限です。
この制約下では、わずか150MBのメモリリークであっても、アドレス空間の断片化(フラグメンテーション)を引き起こし、連続したメモリ領域が確保できなくなって容易にクラッシュします。
上記で提示したAPIベースのメモリプロファイラ(`GetMemoryUsageMB`)を本番環境に組み込み、ログを残すようにしてください。もし処理の過程でメモリが右肩上がりに増加しているならば、それは「変数への代入を怠ったドット連結のコード」がどこかに残っている動かぬ証拠です。
—
エピローグ:システムを「枯らす」ということ
優れたアーキテクトが書くVBAコードは、どれほど大規模なデータを与えられても、メモリグラフ(Working Set)が美しい水平線(フラットライン)を描きます。
「動けばいい」という妥協を排し、COMの裏側で動くC++ライクなオブジェクトモデルの挙動に思いを馳せること。Nothing代入という「退屈なはずの作法」にこそ、システムを極限まで安定させ、数年間にわたりメンテナンスフリーで稼働させるための「職人の知恵」が宿るのです。
