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
ガベージコレクタはこれで完成です。エントリが長くなってしまったので、ガベージコレクタにインスタンス生成を通知する方法などは次のエントリに書きたいと思います。
コード全体はいずれ公開リポジトリにコミットしておきますので、見たいという奇特な人はしばらく待ってください。
