VisualBasic6でGCを作った

VisualBasic6のプログラムがメモリリークを起こしてしまったのでガベージコレクタを作って対処したときの記録です。

状況の説明

VB6は参照カウンタ(reference counter)を用いてインスタンスの寿命を管理するということを忘れていました。つまり、VB6のインスタンスは循環参照するだけで簡単にメモリリークしてしまうのです。

循環参照の例

循環参照の例

普段は.NETやJava等のGCを備えた環境でしかプログラムを作っていないため、思いっきりGCを期待したプログラムを作ってしまいました。

慎重にインスタンスの参照を管理して、GCがなくともメモリリークがおきないようにするのが正当な対処方法なのですが、ものぐさであるためVB6上にGCを実装して対処することにしました。
今考えると余計に大変だったかもしれません(笑)

ガベージコレクタの実装

GCアルゴリズム

ガベージコレクタのアルゴリズムはマーク・アンド・スイープ(Mark-and-Sweep)にしました。理由は実装が簡単であることと、GC中にアプリケーションが止まってしまっても許されたからです。

マークの基点インスタンスの決定方法

.NETやJava等はスタックやスタティック変数からの参照をマークの基点インスタンス(ルートインスタンス)として利用しますが、VB6はルートインスタンスをガベージコレクタが自動的に特定する方法がありません。そこで、アプリケーションがルートインスタンスをガベージコレクタに知らせる方法を採用することにしました。

インスタンス生成の検出方法

.NETやJava等はインスタンスが生成された瞬間をガベージコレクタが検出しGC対象に加えていますが、VB6にはインスタンスの生成をガベージコレクタが自動的に検出する方法もありません(笑)
そこで、GC対象となるべきインスタンスが自分自身でガベージコレクタに生成されたことを知らせる方法を採用することにしました。

GCMarkSweepManagerの実装

さて、ここまで決めればマーク・アンド・スイープガベージコレクタの実装は簡単です。
まず、以下のインタフェースとクラスを用意しました。

クラス図

クラス図

  • <<インタフェース>> GCManager
    • GCを管理するクラスの基底インタフェースです。
    • GCObjectを実装したGC対象のインスタンスを覚えておくメソッド(Insert)を持ちます。
    • ルートインスタンスを追加するメソッド(InsertRoot)を持ちます。
    • ルートインスタンスを削除するメソッド(DeleteRoot)を持ちます。
    • GCを実行するメソッド(Run)を持ちます。
  • <<インタフェース>> GCObject
    • GC対象のインスタンスが実装するべきインタフェースです。
    • マークを行うメソッド(SetMark)を持ちます。
    • マーク状況を返すメソッド(GetMark)を持ちます。
    • GCの破棄対象になってときに自身が保持する参照を破棄するためのメソッド(Dispose)を持ちます。
  • GCMarkSweepManager
    • マーク・アンド・スイープアルゴリズムを用いてGCを実現するGCManagerです。
    • GCNodeクラスを用いてルートインスタンスのリストヘッド(mRoot)を保持しています。
    • GCNodeクラスを用いてGC対象となるインスタンスのリストヘッド(mNode)を保持しています。
    • 最後にマークしたマーク番号(mMark)を保持しています。
  • GCNode
    • GCObjectのlinked-listノードです。
    • 次のノード(NextNode)を保持しています。
    • GC対象となるインスタンス(Object)を保持しています。

GCMarkSweepManagerのRunメソッドでは、呼び出されるたびに0~15までのマークを作成し、MarkメソッドとSweepメソッドを順番に呼び出します。

Private Sub GCManager_Run()
    Dim tMark As Long
    tMark = (mMark Mod 16) + 1
    mMark = tMark
    Call Mark(tMark)
    Call Sweep(tMark)
End Sub

GCMarkSweepManagerのMarkメソッドでは、すべてのルートインスタンスにマークを依頼します。今回の実装ではマークを再帰的に行っています。再帰を使ったマークは実装が簡単ですが、インスタンスのネストが深い場合にスタックオーバーフローが起きる恐れがあります。そのときにはキューを使った実装に置き換える必要があります。

Private Sub Mark(aMark As Long)
    Dim tRoot As GCNode
    Set tRoot = mRoot
    While Not tRoot Is Nothing
        Call tRoot.Object.SetMark(aMark)
        Set tRoot = tRoot.NextNode
    Wend
End Sub

GCMarkSweepManagerのSweepメソッドでは、マークされなかったインスタンスをゴミだと認識します。そして、ゴミをリストから取り除きつつ、Disposeメソッドを呼び出してゴミ掃除します。

Private Sub Sweep(aMark As Long)
    Dim tNode As GCNode
    Dim tObject As GCObject
    Dim tNextNode As GCNode
 
    Set tNode = mNode
    While Not tNode Is Nothing
        Set tObject = tNode.Object
        If tObject.GetMark() = aMark Then
            Set tNextNode = tNode.NextNode
            While Not tNextNode Is Nothing
                Set tObject = tNextNode.Object
                If tObject.GetMark() = aMark Then
                    Set tNode = tNextNode
                    Set tNextNode = tNextNode.NextNode
                Else
                    Set tNextNode = tNextNode.NextNode
                    Set tNode.NextNode = tNextNode
                    Call tObject.Dispose
                End If
            Wend
            Exit Sub
        Else
            Set tNode = tNode.NextNode
            Set mNode = tNode
            Call tObject.Dispose
        End If
    Wend
End Sub

ガベージコレクタはこれで完成です。エントリが長くなってしまったので、ガベージコレクタにインスタンス生成を通知する方法などは次のエントリに書きたいと思います。

コード全体はいずれ公開リポジトリにコミットしておきますので、見たいという奇特な人はしばらく待ってください。

コメントをどうぞ


ホーム | RSS | 採用情報 | 会社情報