t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA オブジェクトとメモリの関係 ~ オブジェクトが参照型と呼ばれる理由

前回、VBAを擬人化して、変数が記号表によって管理されているというところまで書いた。

※読んでない方はこちら
thom.hateblo.jp

さて、今回はオブジェクトがメモリ上でどう扱われるのかという話。

次のコードで説明しようと思う。

Dim c As Collection
Set c = New Collection

オブジェクトはメモリにどのように存在するのか

前回の話では、値がメモリに入っていて、記号表によって変数名とアドレスを対応付けていると説明した。

オブジェクトの場合もこんな風になっているんだろうか。
f:id:t-hom:20161006173751p:plain

実はこの図は間違いで、Integer型やLong型のように直接値が入っているわけではない。
f:id:t-hom:20161006174056p:plain

オブジェクト型変数の場合、記号表からアドレスを参照した先には、更に別のメモリ領域を指すアドレスが入っていてそこにオブジェクトの実体が居る。
f:id:t-hom:20161006173251p:plain

図中にスタックメモリ・ヒープメモリと書いたが、これはメモリの種類が違うわけではなく、同じメモリでスタック領域・ヒープ領域と区画が分けられているだけである。ちなみにメモリの領域としては他に実行中のプログラム自体が保存されるプログラム領域、グローバル変数などが保存される静的領域がある。

今回メモリ領域の違いについては詳しく説明しないが、普段ローカル変数で使ってるのがスタック領域、オブジェクトが保存されるのがヒープ領域ということ。

オブジェクト変数が宣言されてからオブジェクト(実体)がセットされるまでの流れ

さて、先ほどの図では変数cにCollectionが入った状態のメモリ図を見せたけれど、今回はコードを順に追いながら説明する。
まずDim c As Collectionが実行されると、インタープリターはスタックメモリに4バイト確保し、その中身にゼロをセットする。
f:id:t-hom:20161006175811p:plain

オブジェクト型の変数は中身が参照先アドレスである。その参照先アドレスが0というのはつまり何も参照していない状態。これが実はVBAのNothingの正体である。

次にSet c = New Collectionが処理される。まず代入式の右辺にある「New Collection」により、ヒープ領域にCollectionの実体が作られる。
f:id:t-hom:20161006180906p:plain

そして作成されたCollectionの実体を指すアドレスが、変数cが指すスタックメモリに書き込まれる。
f:id:t-hom:20161006181405p:plain

以上がオブジェクト変数を宣言して新規オブジェクトを作成した際のメモリの動きである。

二つの変数に同じオブジェクトを参照させる

次に、もう一つCollection型の変数dを作成し、その変数dに変数cをセットしたケースを説明する。

Dim c As Collection
Set c = New Collection
Dim d As Collection
Set d = c

とりあえずDim d As Collectionまで完了したのが次の図。
f:id:t-hom:20161006183042p:plain

次に、Set d = cを処理する。
f:id:t-hom:20161006183337p:plain

すると、変数dと変数cは同じオブジェクトを指すことになる。
f:id:t-hom:20161006183957p:plain

次に、先ほどのコードに1行加えて、Set c = Nothingとしてみよう。

Dim c As Collection
Set c = New Collection
Dim d As Collection
Set d = c
Set c = Nothing

オブジェクトが破棄されるタイミング

Nothingをセットするとオブジェクトが破棄されると教えられた人も居るだろう。でもその理解は正しくない。
Set c = Nothingとすると変数cからオブジェクトへの参照が無くなるだけで、dからの参照は有効なままである。
f:id:t-hom:20161006190122p:plain

オブジェクトが破棄されるのは、そのオブジェクトがどこからも参照されなくなった時である。
プロシージャが終了するとローカル変数はすべて破棄されるので、変数dも消滅する。するとdからオブジェクトへの参照がなくなり、Collectionはどこからも参照されていない状態となる。そして最終的にVBAによって片づけられる。

このようにオブジェクトへの参照が0になった時点で破棄される管理方式を、参照カウント方式と呼ぶ。

循環参照を作るとオブジェクトが破棄されなくなる。

参照カウント方式というのは一つ弱点があって、オブジェクト同士で循環参照を作ってしまうと参照カウントが0にならずにいつまでもメモリに残ってしまう。
f:id:t-hom:20161006192400p:plain

コードを書いて実証してみよう。
まずは、循環参照ではないバージョンから。

Sub Sample()
    For i = 1 To 100000
        Call  非循環参照
    Next
End Sub

Sub 非循環参照()
    Dim c As Collection
    Set c = New Collection
    Dim d As Collection
    Set d = New Collection
    c.Add New Collection
    d.Add New Collection
End Sub

cとdにそれぞれ新しいコレクションをセットする「非循環参照」マクロを、Sampleマクロから10万回呼んでみる。

実行前のメモリ使用量は約14メガバイト
f:id:t-hom:20161006193109p:plain

そしてマクロを実行してみるが、大して変わらない。
f:id:t-hom:20161006193225p:plain

次に、循環参照させてみる。

Sub Sample()
    For i = 1 To 100000
        Call 循環参照
    Next
End Sub

Sub 循環参照()
    Dim c As Collection
    Set c = New Collection
    Dim d As Collection
    Set d = New Collection
    c.Add d
    d.Add c
End Sub

このコードでは、コレクションcがdを保持し、コレクションdがcを保持することになる。

実行前のメモリ使用量は約14メガバイト
f:id:t-hom:20161006193411p:plain

実行後は、、なんと43メガバイト
f:id:t-hom:20161006193514p:plain

もう一度実行してみると、、73メガバイト
f:id:t-hom:20161006193555p:plain

とまあ、こんな感じで、循環参照させてると参照カウントが減らないということが分かった。

ちなみに、こんな風に最後にNothingを代入しても結果は同じである。

Sub 循環参照()
    Dim c As Collection
    Set c = New Collection
    Dim d As Collection
    Set d = New Collection
    c.Add d
    d.Add c
    Set c = Nothing
    Set d = Nothing
End Sub

Nothingで消えるのは変数からの参照であって、オブジェクト同士に持たせた参照は消えない為だ。
f:id:t-hom:20161006200456p:plain

循環参照させたオブジェクトを最後に破棄したい場合、必要なのは変数へのNothingではなくて、循環参照の解消である。
つまり、コレクションcにAddしたdをRemoveしてやれば良い。

Sub 循環参照()
    Dim c As Collection
    Set c = New Collection
    Dim d As Collection
    Set d = New Collection
    c.Add d
    d.Add c
    c.Remove 1
End Sub

すると、下図のようになり、
f:id:t-hom:20161006201219p:plain

プログラム終了で変数c、変数dが破棄され、
f:id:t-hom:20161006201342p:plain

どこからも参照されていない図の下のコレクションが破棄され、
f:id:t-hom:20161006201418p:plain

参照を失ったもう一つのコレクションも破棄される。
f:id:t-hom:20161006201629p:plain

これらの実験はExcelを終了させればメモリは解放されるので、そんなに危険はない。実際にやってみたい方はどうぞ。

なお、JavaとかC#とかVB.Netは参照カウント方式じゃなくてガベージコレクション(ごみ集め)という方式を採用している。
ガベージコレクション方式では、たとえヒープ領域内で循環参照していてもスタック領域から参照されていないオブジェクトは一定時間ごとに自動的に破棄してくれる。
巡回お掃除ロボが備わってるわけだ。うらやましい。

まとめと補足

オブジェクトはヒープメモリに存在し、オブジェクト変数の中身は値ではなくオブジェクトのアドレスが入っている。このような性質から、オブジェクト型は参照型に分類される。
オブジェクト変数にオブジェクト変数を代入するとアドレスのコピーになるので、結果的に参照先のオブジェクトは同じである。
何処からも参照されなくなったオブジェクトは結果的に破棄されるのであって、Nothingを代入することで直接オブジェクトが破棄されるわけではない。
オブジェクトの参照を保持できるのは変数の他に、With文やCollectionや配列などがある。

【参考】変数を作らずにWith文に直接新規オブジェクトを保持させることもできる。
thom.hateblo.jp
この場合、End Withで参照が破棄されるので、オブジェクトを生かすにはコレクションや別の変数に参照を保持させると良い。

補足として、今回ヒープ領域にCollectionを8バイトとして格納した図を用いたが、あくまでサンプルなので8バイトというのはデタラメである。実際にはCollectionオブジェクトのItemプロパティは更に先頭アイテムのアドレスを保持しており、それぞれのアイテムはヒープ上にバラバラに存在していると思われる。

また、AddやRemoveなどのメソッドは各オブジェクト内に存在するわけではなく、全Collectionオブジェクトで共通である。以下の書籍によるとオブジェクトのメソッドはメソッドエリア(静的領域)に存在し、オブジェクトを量産してもメソッドが占めるメモリ領域は一か所のみとなるそうだ。

オブジェクト指向でなぜつくるのか 第2版

オブジェクト指向でなぜつくるのか 第2版

ただし、実際にVBAでどのようにメモリ上にオブジェクトが展開されるかは不明。
今回は解説があまり複雑にならないよう、オブジェクトはヒープ領域に存在すると書いたが、厳密にはオブジェクトからさらにメソッドへの参照やアイテムへの参照が張り巡らされているイメージ。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。