【テクニカル・上級編】ADOの接続プーリングを意識した、長時間稼働アプリのメモリリーク対策 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

COMの深淵:長時間稼働AccessアプリにおけるADO接続プーリングとメモリ管理の極致

業務の屋台骨として、数ヶ月、あるいは数年にわたって稼働し続けるAccessアプリケーション。現場のシニアエンジニアなら、一度は経験しているはずだ。ある日突然、メモリ使用量が右肩上がりに増大し、最終的に「メモリ不足」という無慈悲なダイアログと共に、システムが沈黙する瞬間を。

多くの開発者は、これを単なる「VBAの限界」や「Accessの設計ミス」として片付けようとする。しかし、それは浅い。真のアーキテクトであれば、その背後にあるCOM(Component Object Model)の参照カウント、OLE DBプロバイダーの接続プーリング、そしてWindows OSレベルのメモリ管理の連鎖的な崩壊を見抜かなければならない。

本稿では、ADOを用いたシステム間連携において、いかにしてオブジェクトのライフサイクルを完全に掌握し、メモリリークを根絶するか。その極限の知見を共有する。

1. 破綻のメカニズム:なぜ `Set obj = Nothing` だけでは足りないのか

初学者は、オブジェクトを使い終えた後に `Set obj = Nothing` と書けば、メモリは解放されると信じている。しかし、これは致命的な誤解だ。

`Set obj = Nothing` が行っているのは、あくまでその変数に対する参照カウント(Reference Count)を1減らす操作に過ぎない。もし、その `Connection` オブジェクトに紐付いた `Recordset` オブジェクトが、別の変数や、あるいはエラーハンドリングの不備によってメモリ上に残存していれば、参照カウントはゼロにならず、オブジェクトは「ゾンビ」としてメモリに居座り続ける。

さらに厄介なのが、ADOが利用するOLE DBの接続プーリングだ。
ADOはパフォーマンス向上のため、一度確立した物理接続をプロバイダー側にプールする。適切に `Close` 処理が行われず、かつオブジェクトの参照が残ったままになると、プーリング機構は「この接続はまだ使用中である」と判断し、物理的なリソース(ソケットやセッション)を解放しない。これが積み重なり、最終的にシステム全体のハンドル数とメモリを食いつぶすのである。

2. 鉄則:決定論的なオブジェクト破棄の実装

長時間稼働アプリにおいて、オブジェクトのライフサイクルは「非決定論的(いつ消えるかわからない)」であってはならない。必ず「決定論的(コードのこの行で必ず消える)」でなければならない。

以下に、私が設計指針として推奨する、堅牢なADO接続ラッパーの構造を示す。

' --- Module: modDatabaseEngine ---
Option Explicit

' 接続のライフサイクルを厳格に管理するためのクラスモジュール(概念例)
' 実際にはクラスモジュールを作成して実装すること

Public Sub ExecuteSecureQuery(ByVal strSql As String)
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    
    ' エラーハンドリングの徹底:エラー発生時も必ずクリーンアップへ飛ばす
    On Error GoTo ErrorHandler

    ' 1. オブジェクトの初期化
    Set conn = New ADODB.Connection
    Set rs = New ADODB.Recordset

    ' 2. 接続設定(Connection Poolingを意識したプロパティ設定)
    ' ConnectionTimeoutを適切に設定し、ネットワークの瞬断によるハングを防ぐ
    conn.ConnectionString = "Provider=SQLOLEDB;Data Source=SERVER_NAME;Initial Catalog=DB_NAME;Integrated Security=SSPI;"
    conn.ConnectionTimeout = 15 
    conn.Open

    ' 3. 実行
    rs.Open strSql, conn, adOpenStatic, adLockReadOnly

    ' --- ここにビジネスロジックを記述 ---
    If Not rs.EOF Then
        Debug.Print rs.Fields(0).Value
    End If
    ' ----------------------------------

CleanUp:
    ' 4. 【極めて重要】決定論的な破棄の順序
    ' 常に「子(Recordset)」から先に「親(Connection)」を閉じる
    
    On Error Resume Next ' クリーンアップ中のエラーでプロセスを止めない
    
    If Not rs Is Nothing Then
        If rs.State <> adStateClosed Then rs.Close
        Set rs = Nothing
    End If
    
    If Not conn Is Nothing Then
        If conn.State <> adStateClosed Then conn.Close
        Set conn = Nothing
    End If
    
    Exit Sub

ErrorHandler:
    ' エラーログ出力などの処理
    MsgBox "Critical Error: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

アーキテクトの視点:破棄の順序

オブジェクトの破棄には「階層的な逆順」という鉄則がある。
`Connection` → `Command` → `Recordset` の順で生成したならば、`Recordset` → `Command` → `Connection` の順で破棄しなければならない。親を先に `Nothing` にして参照を断ち切ってしまうと、子オブジェクトが親のメモリ領域を不適切に参照し続け、アクセス違反(Access Violation)を引き起こす原因となる。

3. Windows APIによるメモリ監視:事実を確認する

「メモリリークしている気がする」という主観は、エンジニアリングにおいては無価値だ。Windows APIを用いて、プロセスの物理メモリ使用量(Working Set)を定量的に監視せよ。

以下のコードは、Accessプロセスの現在のメモリ使用量をバイト単位で取得する。これを定期的にログ出力することで、リークの発生タイミングを特定できる。

' --- Module: modSystemMonitor ---
Option Explicit

' Windows APIの宣言
Private Type PROCESS_MEMORY_COUNTERS
    cb                         As Long
    PageFaultCount             As Long
    PeakWorkingSetSize         As LongLong ' 64bit環境を考慮
    WorkingSetSize             As LongLong
    QuotaPeakPagedPoolUsage    As LongLong
    QuotaPagedPoolUsage        As LongLong
    QuotaPeakNonPagedPoolUsage As LongLong
    QuotaNonPagedPoolUsage     As LongLong
    PagefileUsage              As LongLong
    PeakPagefileUsage          As LongLong
End Type

Private Declare PtrSafe Function GetCurrentProcess Lib "kernel32" () As LongPtr
Private Declare PtrSafe Function GetProcessMemoryInfo Lib "psapi.dll" ( _
    ByVal hProcess As LongPtr, _
    ByRef lpieners As PROCESS_MEMORY_COUNTERS, _
    ByVal cb As Long) As Long

''' <summary>
''' 現在のプロセスのWorking Set(物理メモリ使用量)を取得する
''' </summary>
Public Function GetCurrentWorkingSet() As LongLong
    Dim pmc As PROCESS_MEMORY_COUNTERS
    Dim hProcess As LongPtr
    Dim ret As Long
    
    hProcess = GetCurrentProcess()
    pmc.cb = Len(pmc)
    
    ret = GetProcessMemoryInfo(hProcess, pmc, pmc.cb)
    
    If ret <> 0 Then
        GetCurrentWorkingSet = pmc.WorkingSetSize
    Else
        GetCurrentWorkingSet = 0
    End If
End Function

' 使用例
Sub MonitorMemory()
    Dim mem As LongLong
    mem = GetCurrentWorkingSet()
    Debug.Print "Current Working Set: " & Format(mem / 1024 / 1024, "0.00") & " MB"
End Sub

*注: `LongLong` 型および `PtrSafe` 属性は、64bit版Office環境を前提としている。レガシーな32bit環境の場合は、型を `Long` に適宜読み替えること。*

4. 長時間稼働を支える設計思想

メモリリーク対策は、コードの書き方だけでは完結しない。システム全体の設計思想に組み込む必要がある。

1. ステートレスな接続管理:
接続をグローバル変数(Public変数)に保持して使い回すのは、小規模なツールでは効率的だが、長時間稼働アプリでは「毒」となる。接続は「必要な時に開き、用が済んだら即座に閉じる」というステートレスな原則を徹底せよ。
2. DoEventsの乱用禁止:
UIのフリーズを防ぐために `DoEvents` を多用する者がいるが、これは再入可能性(Re-entrancy)の問題を引き起こし、予期せぬタイミングでイベントが発生してオブジェクトの参照カウントを狂わせる。
3. エラーハンドリングの「全方位」展開:
エラーが発生した際に、オブジェクトの解放処理をスキップして `Exit Sub` するような実装は、プロの仕事ではない。すべての出口(Exit)において、クリーンアップが保証されていることを設計段階で担保せよ。

結論

Access VBAは、決して「使い捨ての道具」ではない。適切に設計されたCOMオブジェクトのライフサイクル管理と、OSレベルの挙動を理解した実装があれば、それは極めて堅牢なエンタープライズ・エンジンへと昇華する。

メモリの増大を「仕様」として受け入れるな。それは、設計の不備が発する悲鳴である。その悲鳴を、APIによる計測と厳格な破棄ロジックによって、静寂へと変えること。それが、我々アーキテクトに課せられた責務である。

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