一覧PG-19-3 · 翻訳処理とメモリの入口

参照カウントとガベージコレクション

一つのオブジェクトを別名とリストから保持し、参照を外したときの変化を確認します。続いて二つのオブジェクトを互いに参照させ、外部から到達できる場合とできない場合で、回収結果がどう変わるか調べます。

説明

オブジェクトは、変数名やコンテナなどから参照されている間は利用されます。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

1
class Node: def __init__(self, name): self.name = name self.next = None
2
import sys, weakref, gc; node = Node("A"); watch = weakref.ref(node); baseline = sys.getrefcount(node); print(baseline, watch() is node)
2 True
3
alias = node; holder = [node]; print(sys.getrefcount(node) - baseline)
2
4
del node; del alias; print(watch() is None); holder.clear(); print(watch() is None)
False
True
5
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
6
collected = gc.collect(); print(wa() is None, wb() is None); print(root[0].name, root[0].next.name)
False False
A B
7
root.clear(); print(wa() is None, wb() is None)
False False
8
collected = gc.collect(); print(wa() is None, wb() is None)
True True
9
if was_enabled: gc.enable() print(gc.isenabled())
True

演習完了