著者: 禅とコンピュータープログラミングの芸術
1 はじめに
自動メモリ管理としても知られるガベージ コレクションは、コンピュータ サイエンスの重要な分野であり、コンパイラ、インタプリタ、オペレーティング システムなどと密接に関連しています。
まず、ゴミとは何かを理解する必要があります。コンピュータシステムにおいて、ガベージとは、使用されなくなった変数、データ、または空間を指しますが、さまざまな理由によりすぐに解放できない無用なリソースです。ガベージには、割り当てられたものの使用されていないメモリや、特定のコードの実行後に解放されていないシステム リソースが含まれます。
次に、なぜガベージ コレクションが必要なのでしょうか? 主に次の 2 つの側面があります。
1. メモリ管理。プログラムの実行中に、割り当てられたメモリが多すぎて長時間アクセスできない場合、メモリ リークが発生し、最終的にはシステム パフォーマンスの低下やクラッシュにつながることがあります。したがって、ガベージ コレクション メカニズムは、この問題を解決するように設計されています。システム内の不要なメモリを効果的に解放できるため、システムのリソース使用率が向上します。
2. 資源の無駄を削減します。多くの場合、システムには多数のファイルまたはオブジェクトが存在しますが、これらのオブジェクトがメモリ内に保持されると、断片化が大量に発生し、システムのパフォーマンスが低下します。したがって、ガベージ コレクションは資源の無駄を減らすためにも行われます。
最後に、ガベージ コレクションには、参照カウントとマーク アンド スイープという 2 つのタイプがあります。この記事では、これら 2 つのガベージ コレクション メソッドとその具体的な実装をそれぞれ紹介します。
2. 参照カウント方法
参照カウントは、最も単純なガベージ コレクション方法です。その基本的な考え方は、各オブジェクトへの参照の数を追跡することです。オブジェクトへの参照の数がゼロになると、そのオブジェクトはもう使用されず、リサイクルできることになります。この方法は比較的シンプルで実装が簡単で、非常に効率的です。しかし、循環参照の問題という欠陥があり、この場合、参照カウントがこの問題を解決できない場合は、他の手段 (たとえば、ヒープ サイズの拡大や縮小など) によってのみ解決できます。したがって、複雑なデータ構造 (グラフィカル ユーザー インターフェイスのウィンドウ システムなど) の場合、この方法は正しく機能しません。さらに、完全に自動ではなく、参照カウントを正しく実装するにはプログラマに依存する必要があるため、実用性も制限されます。
具体的な実装は以下の通りです。
class Object(object):
def __init__(self):
self._refcount = 0
def inc_ref(self):
self._refcount += 1
def dec_ref(self):
if self._refcount > 0:
self._refcount -= 1
@property
def refcount(self):
return self._refcount
def is_gcable(self):
return self._refcount == 0
def mark():
# mark phase - traverse the object graph and find all reachable objects
visited = set() # to avoid visiting same object twice
queue = [] # contains root objects (objects with zero reference count)
for obj in gc.get_objects():
if not isinstance(obj, Object):
continue # skip non-Object types
if id(obj) in visited:
continue # already visited this object
if obj.is_gcable():
queue.append(obj)
visited.add(id(obj))
else:
obj.inc_ref() # increment reference count of other objects
# enqueue any remaining unreachable objects
while queue:
obj = queue.pop(0)
for referrer in gc.get_referrers(obj):
if id(referrer) not in visited:
if hasattr(referrer, '_reachable'):
getattr(referrer, '_reachable').append(obj)
else:
setattr(referrer, '_reachable', [obj])
queue.append(referrer)
visited.add(id(referrer))
delattr(obj, '_reachable')
def sweep():
# sweep phase - free unreferenced memory
for obj in gc.get_objects():
if not isinstance(obj, Object):
continue # skip non-Object types
if obj.refcount!= 0 or not obj.is_gcable():
continue # skip referenced or non-collectible objects
try:
del obj.__dict__['_refcount'] # delete reference count attribute
except KeyError:
pass # no reference count attribute found
del obj # delete object from heap
その中には、Object
参照カウント属性をカプセル化するために使用されるカスタム クラスがあり、参照_refcount
カウントを増加、減少させ、取得するための 3 つのメソッド と を定義しています。オブジェクトがどのオブジェクトからも参照されておらず、リサイクルできることを示す True を返すメソッドもあります。inc_ref()
dec_ref()
@property refcount()
is_gcable()
mark()
このメソッドはオブジェクト グラフ全体を走査し、到達可能なすべてのオブジェクト (つまり、参照カウントが 0 より大きいオブジェクト) を見つけます。次に、これらの到達可能なオブジェクトがキューに入れられ、キューの先頭にあるオブジェクトが順番に取り出され、それらのオブジェクトによって参照されるすべてのオブジェクトが、すべての到達可能なオブジェクトが処理されるまでさらに処理されます。オブジェクトが参照されていない場合は、それを直接参照するすべてのオブジェクトをキューの最後に追加します。参照されていない場合は、_reachable
オブジェクトのプロパティ リストに追加します。このようにして、オブジェクトへのすべての参照が処理されると、そのオブジェクトを安全に削除できます。
sweep()
このメソッドは、参照されていないオブジェクトのメモリを解放するために使用されます。すべてのオブジェクトを走査し、その参照カウントが 0 でリサイクルできないかどうかを確認し ( )、その属性を削除します (カスタム クラスはそうではないことに注意してnot obj.is_gcable()
ください__dict__['_refcount']
。Object
プロパティ__slots__
はで宣言されているため、プロパティを通じてプロパティ値を設定または取得する_refcount
必要があります)。__dict__
次に、これらのオブジェクトを削除します。
ここでは、実行前にすべてのオブジェクトが確実に処理されるようにする方法、さまざまな状況に応じてメソッドをsweep()
呼び出すタイミングを決定する方法など、注意が必要な詳細がいくつかあります。sweep()
ただし、一般的なプロセスは次のとおりです。
3. マークアンドクリア方法
マーク アンド スイープは、もう 1 つの古いガベージ コレクション手法であり、その基本的な考え方は、すべての生きているオブジェクトにマークを付けてから、マークされていないオブジェクトをすべてクリアすることです。この方法は参照カウント方法よりも複雑ですが、不連続なメモリの断片が多数生成されることと、マーク アンド クリア方法では循環参照の問題を十分に解決できないという欠点があります。したがって、相対的に言えば、参照カウントは一般的なプログラミング言語に適しています。
具体的な実装は以下の通りです。
import sys
class Object(object):
pass
def mark():
global MARKED
MARKED = set([None]) # initialize marked list
# mark phase - traverse the object graph and mark all reachable objects
stack = [root] # contains root objects (objects with zero reference count)
while stack:
obj = stack.pop()
if id(obj) not in MARKED:
MARKED.add(id(obj))
# push all referrents onto the marking stack
stack.extend([r for r in gc.get_referents(obj)
if type(r) is not type])
def sweep():
global MARKED
# collect unused memory by removing unmarked objects
nbytes_freed = 0
objs = [o for o in gc.get_objects() if isinstance(o, Object)]
for obj in objs:
if id(obj) not in MARKED:
size = getsize(obj)
del obj # remove object from heap
nbytes_freed += size
MARKED = set() # reset marked list
return nbytes_freed
ここでは、モジュールの紹介を除いてsys
、残りの内容はほぼ同じです。Object
このクラスは新しい機能を追加するものではなく、 を継承するだけですobject
。
mark()
このメソッドはスタックを走査し、検出された各オブジェクトは検出済みとしてマークされます。次に、到達可能なすべての (参照カウントが 0 より大きい) オブジェクトの参照をスタックにプッシュし、探索を続けます。
sweep()
このメソッドは、マークされていないオブジェクトによって占有されているメモリを解放します。最初にすべてのObject
種類のオブジェクトを取得し、次にこれらのオブジェクトを走査します。マークされていない場合は、ヒープから削除し、解放されたバイト数を記録します。最後にマークセットを空にリセットし、解放されたバイト数を返します。
ただし、この実装にはまだいくつかの問題があります。まず、これはマーク アンド クリア アルゴリズムであるため、到達可能なすべてのオブジェクトを見つけるためにディスク全体をスキャンする必要があり、効率の悪いプロセスです。次に、メモリがオブジェクトごとに解放されるため、断片化が発生する可能性があります。最後に、オブジェクトは 1 つずつ解放されるため、すべてのメモリを一度に解放できない場合があるため、最大限のリサイクル効果を得るには GC を複数回トリガーする必要があります。
要約すると、マーク アンド クリア方式にはいくつかの欠点もありますが、その利点は理解しやすく、実装が比較的簡単であることです。したがって、非常に要求の厳しい一部のアプリケーション シナリオでは、マーク アンド クリア方式が依然として役割を果たすことができます。