参照カウントとガベージコレクション
一つのオブジェクトを別名とリストから保持し、参照を外したときの変化を確認します。続いて二つのオブジェクトを互いに参照させ、外部から到達できる場合とできない場合で、回収結果がどう変わるか調べます。
説明
オブジェクトは、変数名やコンテナなどから参照されている間は利用されます。CPythonは参照カウントを用い、それだけでは回収できない循環参照を循環GCで補います。
一つのオブジェクトを別名とリストから保持し、参照を外したときの変化を確認します。続いて二つのオブジェクトを互いに参照させ、外部から到達できる場合とできない場合で、回収結果がどう変わるか調べます。
1参照を持つオブジェクト
nameとnextを持つ小さなクラスを定義します。
class Node:観察対象の型
def __init__(self, name):
self.name = name
self.next = None別の対象への参照を置く属性nextには後で別のNodeを代入できます。まずは循環のない一つのオブジェクトで、参照の追加と削除を確認します。
2弱参照で生存を確認する
弱参照は対象を生かし続ける強い参照にはなりません。対象が回収された後は、弱参照を呼ぶとNoneが返ります。
import sys, weakref, gc; node = Node("A"); watch = weakref.ref(node)対象を保持しない観察用の参照; baseline = sys.getrefcount(node)一時的な引数参照も含むカウント; print(baseline, watch() is node弱参照が同じ対象を返すか確認)このCPythonでは基準のカウントは2です。nodeからの参照に加え、getrefcountへ引数として渡す一時的な参照も含みます。絶対値だけで決めつけず、参照の追加前後の差を確認します。
3別名とコンテナが保持する参照
別名とリスト要素を追加し、基準との差を表示します。
alias = node別名から保持; holder = [node]リスト要素から保持; print(sys.getrefcount(node) - baseline追加前からの増分)差は2です。aliasとholderの要素が、それぞれ同じNodeを強く参照しています。オブジェクトをコピーしたのではなく、保持する経路を増やしました。
4参照を順に外す
名前を削除した後もリストから保持されていることを確認し、最後にリストを空にします。
del node; del alias二つの名前を削除; print(watch() is Noneまだ生存しているか確認); holder.clear()最後の強い参照を外す; print(watch() is None回収後の弱参照)名前を二つ削除しても、リストに残っている間はFalseです。リスト要素を外すとTrueになり、対象は回収されています。delは対象を強制的に全ての場所から消す命令ではなく、指定した参照経路を外す操作です。
5自動GCを止めて循環を作る
循環GCの自動実行を一時的に止め、AとBを互いに参照させます。rootからAへ到達できる状態を残します。
was_enabled = gc.isenabled(); gc.disable()循環GCの自動実行を一時停止; a = Node("A"); b = Node("B"); a.next = b; b.next = a相互参照を作る; wa = weakref.ref(a); wb = weakref.ref(b); root = [a]外部からの経路を保持; del a, b作成時の名前を外す; print(wa() is None, wb() is None)二つの弱参照はどちらもNoneではありません。root→A→Bという外部からの経路があり、さらにAとBが互いを保持しています。自動GCを止めても、参照カウントによる管理まで止まるわけではありません。
6到達できる循環は残る
明示的にGCを実行します。回収件数には他の対象も含まれ得るため、観察している二つの弱参照で結果を確認します。
collected = gc.collect()明示的に循環GCを実行; print(wa() is None, wb() is None観察対象の生存); print(root[0].name, root[0].next.name外部から到達できる二つの対象)rootからA、Bへ到達できるため、両方とも残ります。循環の有無だけではなく、外部から到達できるかによって回収対象が決まります。
7外部からの経路を外す
rootを空にし、まだ自動GCを止めたままで確認します。
root.clear()最後の外部経路を外す; print(wa() is None, wb() is None相互参照だけで残る状態)両方ともまだ生存しています。外部からの経路は消えましたが、AとBが互いを参照しているため、参照カウントだけでは0になりません。
8到達できない循環を回収する
再び明示的にGCを実行します。
collected = gc.collect()到達できない循環を調べる; print(wa() is None, wb() is None観察対象の回収結果)今度は両方ともTrueです。外部から到達できない循環をGCが検出し、回収しました。参照カウントと循環GCは、このように異なる条件を扱って補い合います。
9自動GCの設定を戻す
演習開始時に自動GCが有効だった場合は、有効へ戻します。
if was_enabled:開始時の設定を確認
gc.enable()自動GCを再開
print(gc.isenabled()復元した状態)このセッションではTrueになり、自動実行へ戻ります。回収の時点はPythonの実装や設定に依存します。ここで確認した即時の参照カウントによる回収は、使用したCPythonでの挙動です。
まとめ
別名やコンテナ要素は、同じオブジェクトを保持する参照を増やします。delは指定した参照を外します。
弱参照は対象の生存を強制しないため、回収前後の観察に使えます。
循環があっても外部から到達できれば残ります。到達できない循環を調べるGCが、参照カウントを補います。
操作履歴と解説を振り返れます。
Python 3
class Node:
def __init__(self, name):
self.name = name
self.next = Noneimport sys, weakref, gc; node = Node("A"); watch = weakref.ref(node); baseline = sys.getrefcount(node); print(baseline, watch() is node)2 True
alias = node; holder = [node]; print(sys.getrefcount(node) - baseline)2
del node; del alias; print(watch() is None); holder.clear(); print(watch() is None)False True
was_enabled = gc.isenabled(); gc.disable(); a = Node("A"); b = Node("B"); a.next = b; b.next = a; wa = weakref.ref(a); wb = weakref.ref(b); root = [a]; del a, b; print(wa() is None, wb() is None)False False
collected = gc.collect(); print(wa() is None, wb() is None); print(root[0].name, root[0].next.name)False False A B
root.clear(); print(wa() is None, wb() is None)False False
collected = gc.collect(); print(wa() is None, wb() is None)True True
if was_enabled:
gc.enable()
print(gc.isenabled())True
演習完了