言語のGC機能と参照カウント (前編)
たまにはちゃんと書いたほうがいいかなと思って書いてみる。
あらまし
原始的な参照カウントベースのガーベジコレクションは、循環参照が発生すると、その参照に含まれるオブジェクトを回収できないという厄介な問題を抱えている。循環参照とは、1つ以上のオブジェクトが環状の参照関係を形成している状態のことで、このような参照を持つオブジェクトは、やがてルート (ある時点で言語ランタイムが管理しているすべてのスコープと考えてもいい) から辿りつけなくなって、解放されずにリークしてしまう。
この問題はいろんな LL 言語に見られる。
Perl の場合
use Devel::Peek qw(Dump); sub make_circular { my $foo = {}; my $bar = {}; my $baz = {}; $foo->{'bar'} = $bar; $bar->{'baz'} = $baz; $baz->{'foo'} = $foo; # <- ここをコメントアウトしてみると? return $foo; } Dump(make_circular); for (0 ... 100000) { make_circular; }
出力:
SV = RV(0x8181214) at 0x8194068
REFCNT = 1
FLAGS = (TEMP,ROK)
RV = 0x8152c28
SV = PVHV(0x8157190) at 0x8152c28
REFCNT = 2
FLAGS = (SHAREKEYS)
IV = 1
NV = 0
ARRAY = 0x8174f10 (0:7, 1153
1:1)
hash quality = 100.0%
KEYS = 1
FILL = 1
MAX = 7
RITER = -1
EITER = 0x0
Elt "bar" HASH = 0x80409109
SV = RV(0x81811e0) at 0x8152b44
REFCNT = 1
FLAGS = (ROK)
RV = 0x8152d48
SV = PVHV(0x81571c0) at 0x8152d48
REFCNT = 1
FLAGS = (SHAREKEYS)
IV = 1
NV = 0
ARRAY = 0x817d890 (0:7, 1:1)
hash quality = 100.0%
KEYS = 1
FILL = 1
MAX = 7
RITER = -1
EITER = 0x0
Elt "baz" HASH = 0xffb60ff2
SV = RV(0x818120c) at 0x8194050
REFCNT = 1
FLAGS = (ROK)
RV = 0x815360c
1185
2142
2736
3198
3759
4221
4650
5112
5574
5838
5838
6432
6894
7356
7818
8280
8709
9303
9897
[1] + done perl test.plメモリ使用量が減らないことは次のようにして確認できる*1。PS は Linux のディストリビューションでは標準の procps のものを使っている。
% perl test.pl & PID=$! && while ps -o sz -p $PID | grep -v "SZ"; do :; done
PHP (5.2) の場合
<?php function &make_circular() { $foo = array(); $bar = array(); $baz = array(); $foo['bar'] = &$bar; # <- ここの参照をはずしてみると? $bar['baz'] = &$baz; $baz['foo'] = &$foo; return $foo; } $tmp = &make_circular(); debug_zval_dump($tmp); for ($i = 100000; --$i >= 0;) { make_circular(); printf("%d\n", memory_get_usage()); } ?>
出力:
array(1) refcount(1){
["bar"]=>
&array(1) refcount(2){
["baz"]=>
array(1) refcount(1){
["foo"]=>
array(1) refcount(2){
["bar"]=>
&array(1) refcount(2){
["baz"]=>
array(1) refcount(1){
["foo"]=>
array(1) refcount(2){
["bar"]=>
*RECURSION*
}
}
}
}
}
}
9928
0656
1136
1616
2096
2576
3064
3568
4048
4528
Ruby の場合
Ruby は参照カウントではなくコンサバなマーク&スイープなGCを採用しているので、循環参照があってもきちんと回収はされる。(サンプルコードが悪いのは勘弁)
def make_circular foo = {} bar = {} baz = {} foo['bar'] = bar bar['baz'] = baz baz['foo'] = foo return foo end # GC.disable # <- コメントをはずしてみると? 10000.times do make_circular() end
% ruby test.rb & PID=$! && while ps -o sz -p $PID | grep -v "SZ"; do :; done
出力:
788 887 887 ... (ずっと同じ値)
Python (CPython) の場合
CPython は循環参照検出つきの参照カウントベースのガーベジコレクションを採用しているので、ちょっと興味深い挙動が垣間見れる。
import gc def make_circular(): foo = {} bar = {} baz = {} foo['bar'] = bar bar['baz'] = baz baz['foo'] = foo # <- (1) return foo # gc.disable() # <- (2) for i in xrange(0, 1000000): make_circular()
まず、上のコードを例によって素直に走らせると、
% python test.py & PID=$! && while ps -o sz -p $PID | grep -v "SZ"; do :; done [1] 3968 840 1128 1128 1128 1128 1128 ...
のようになって、きちんと循環コレクタ (cycle collector) が動いていることが分かる。
(2) のコメントを外すと、今度は
% python test.py & PID=$! && while ps -o sz -p $PID | grep -v "SZ"; do :; done [1] 4034 841 1389 4574 5159 6914 7824 10359 14129 ...
のようにメモリ使用量はどんどん増えていく。
(1) をコメントアウトし、(2) のコメントを外した状態にすると、今度は
938 1128 1128 1128 1128 1128 ...
という結果になる。gc.disable() で停止するのは循環コレクタで、循環コレクタが動いていなくても、従来通り参照カウントだけでオブジェクトの破棄タイミングが明らかな場合は都度解放するようになっているから。(厳密にいうと違うけど)
(中編につづく)
*1:mstat はビルドによっては使えないため
