メモリーマネージャーとは
Pythonはインタプリタ言語、コンパイル言語、対話型言語、オブジェクト指向スクリプト言語の高レベルな組み合わせとして、ほとんどのプログラミング言語とは異なり、Pythonの変数は、事前に宣言する必要はありません、変数は型を指定する必要はありません、プログラマはメモリ管理を気にする必要はありません、Pythonインタプリタは、あなたに自動回復を与えます。Pythonインタプリタが自動的にメモリを回収してくれるのです。 開発者はメモリ管理の仕組みについてあまり気にする必要がなく、複雑なメモリ管理作業はすべてPythonのメモリマネージャが引き受けてくれます。
メモリは2つの部分の作成と破壊以上のものではありません、この記事では、解析の2つの部分のpythonのメモリプールとごみ収集に焦点を当てます。
Python
メモリプールを導入する理由
少量のメモリを消費するオブジェクトを大量に作成する場合、new/mallocを頻繁に呼び出すとメモリの断片化が大量に発生し、効率が低下します。メモリプールの目的は、あらかじめ一定数の等しいサイズのメモリブロックを要求しておき、新たなメモリ要求があったときに、まずその要求分のメモリをプールから確保し、足りなくなったら新たなメモリを要求することです。この最大のメリットは、メモリの断片化を減らし、効率を向上させることです。
python のメモリ管理機構は Pymalloc です。
メモリープールの仕組み
まず、CPython(pythonインタプリタ)のメモリアーキテクチャの図を見てください:
- Pythonのオブジェクト管理は主にLevel+1からLevel+3にあります。
- レベル+3: pythonの組み込みオブジェクトは個別のプライベートメモリプールを持ち、メモリプールはオブジェクト間で共有されません。
- Level+2: 要求されたメモリサイズが256KB未満の場合、メモリ確保は主にPythonオブジェクトアロケータによって行われます。
- Level+1: 要求されたメモリサイズが256KBより大きい場合、Pythonのネイティブメモリアロケータによって割り当てられます。
メモリの解放に関して、Pythonはオブジェクトの参照カウントが0になるとデストラクタを呼び出します。デストラクタを呼び出しても、メモリ空間を解放するためにfreeが呼び出されるとは限りません。そのため、デストラクタでもメモリプールの仕組みが使われ、メモリプールから要求されたメモリはメモリプールに戻され、頻繁な要求と解放の動作を避けるようになっています。
ゴミ収集メカニズム
Pythonのゴミ収集機構は、参照カウント機構を主要な戦略として採用し、マークアンドクリーン機構と世代的な回収機構によって補われています。このうち、マークアンドクリア機構は、メモリから解放できない循環参照につながる参照カウントの問題を解決するために使われ、世代リサイクル機構は、ごみ収集の効率を向上させるために使われます。
参照カウント
Pythonは参照カウント、つまりオブジェクトが他のオブジェクトから何回参照されたかを記録することで、メモリ上の変数を管理しています。
Python には参照カウンタと呼ばれる内部追跡変数があります。オブジェクトの参照カウントが0になると、そのオブジェクトはゴミ収集キューに入れられます。
>>> a=[1,2]
>>> import sys
>>> sys.getrefcount(a) ## オブジェクトaの参照カウントを取得する
2
>>> b=a
>>> sys.getrefcount(a)
3
>>> del b ## への参照を削除する。
>>> sys.getrefcount(a)
2
>>> c=list()
>>> c.append(a) ## コンテナへの追加
>>> sys.getrefcount(a)
3
>>> del c ## コンテナの削除、参照-1
>>> sys.getrefcount(a)
2
>>> b=a
>>> sys.getrefcount(a)
3
>>> a=[3,4] ## 再割り当て
>>> sys.getrefcount(a)
2
注意:getrefcountの引数としてaが渡されると、一時参照が作成されるため、結果は+1されます。
- リファレンス数が増加した場合:
- オブジェクトに新しい名前が割り当てられます。
- 容器に入れる
- 参照カウントが減るケース:
- delステートメントを使用したオブジェクト・エイリアスの明示的破棄
- オブジェクトが存在するコンテナが破壊された、またはオブジェクトがコンテナから取り除かれた場合。
- 範囲外の参照、または再割り当て
参照カウントはほとんどのゴミ収集の問題を解決しますが、2つのオブジェクトが互いに参照している場合、del文は参照数を減らすことができますが、参照カウントは0にならず、オブジェクトは破棄されないため、メモリリークが発生します。このような状況に対処するために、Pythonはマークアンドクリーン機構を導入しています。
マーククリア
Mark-Clearは、参照カウントメカニズムによって生成される循環参照の問題を解決するために使用されます。 循環参照は、辞書、タプル、リストなどのコンテナ・オブジェクトでのみ発生します。
その名の通り、ゴミ収集の際に2つのステップに分かれる仕組みです:
- マーキングフェーズでは、すべてのオブジェクトを反復処理し、もしそのオブジェクトが到達可能であれば、つまりそのオブジェクトを参照しているオブジェクトがまだ存在すれば、そのオブジェクトを到達可能としてマークします。
- クリア・フェーズでは、オブジェクトが再度トラバースされ、到達可能としてマークされていないオブジェクトが見つかった場合、そのオブジェクトはリサイクルされます。
>>> a=[1,2]
>>> b=[3,4]
>>> sys.getrefcount(a)
2
>>> sys.getrefcount(b)
2
>>> a.append(b)
>>> sys.getrefcount(b)
3
>>> b.append(a)
>>> sys.getrefcount(a)
3
>>> del a
>>> del b
- aがbを参照し、bがaを参照する場合、2つのオブジェクトはそれぞれ2回参照されます。
- del実行後、オブジェクトa,bの参照数はすべて-1、この時、それぞれの参照カウンタはすべて1、循環参照に引っかかります。
- マーク:両端aのいずれかを見つけ、それがbへの参照を持っているので、次にbへの参照を-1として数えます。
- マーク:その後、bへの参照に沿って、bはaへの参照を持って、参照カウント-1になり、この時間は、オブジェクトのaとbの参照時間はすべて0ですが、到達不能としてマークされます
- Clear: 到達不可能とマークされたオブジェクトは、本当に解放する必要があるものです。
上記のゴミ収集フェーズでは、アプリケーション全体を一時停止し、アプリケーションを再開する前にマーカーがクリアされるのを待ちます。アプリケーションの一時停止時間を短縮するために、Pythonは "世代リサイクル "によって空間と時間を交換することで、ゴミ収集の効率を向上させています。
世代リサイクル
世代リサイクルは、プログラムの場合、寿命の短いメモリ・ブロックが一定の割合で存在し、残りのブロックはプログラムの開始から終了まで、寿命が長いという統計的事実に基づいています。通常、寿命の短いオブジェクトの割合は80%から90%です。したがって、単純に考えると、オブジェクトが長く存在すればするほど、そのオブジェクトがゴミでない可能性が高くなり、回収する量も少なくなるはずです。こうすることで、マークアンドクリーンアルゴリズムの実行中にトラバースするオブジェクトの数を効果的に減らすことができ、ゴミ収集の速度を上げることができます。
Pythonはすべてのオブジェクトを3つの世代に分類します。すべての新しいオブジェクトはデフォルトで第 0 世代のオブジェクトです。第 0 世代で gc スキャンに耐えたオブジェクトは第 1 世代に移動し、第 1 世代で gc スキャンに耐えたオブジェクトは第 2 世代に移動します。
gcスキャンカウント
ある世代で割り当てられたオブジェクトと解放されたオブジェクトの差がある閾値に達すると、現在の世代のgcスキャンが開始されます。世代がスキャンされると、それより若い世代もスキャンされるため、第2世代のgcスキャンが発生すると、第0,1世代のgcスキャンも発生します。
>>> import gc
>>> gc.get_threshold() ## 世代リサイクル機構のパラメータ閾値設定
(700, 10, 10)
- 700 = 新しく割り当てられたオブジェクトの数 - 解放されたオブジェクトの数、0世代gcスキャンがトリガされます。
- 最初の10回:第0世代のgcスキャンが10回発生し、その後第1世代のgcスキャンがトリガーされます。
- 10回目:第1世代のgcスキャンが10回発生した後、第2世代のgcスキャンがトリガーされます。
考察
mark-clearにおいて、del操作の実行後にオブジェクトcがa,も参照していた場合はどうなりますか?
オブジェクトa,b,cの参照関係を以下に示します:
>>> a=[1,2]
>>> b=[3,4]
>>> c=a
>>> a.append(b)
>>> b.append(a)
- ref_count参照カウントの表現
- オブジェクトa,b,cはすべて到達可能。
delを実行すると、参照関係は以下のようになります:
>>> del a
>>> del b
- a,b,c参照_count
gcスキャンの実行
マーカー:aがbを参照し、bのref_countを1減らして0に、bがaを参照し、aのref_countを1減らして1に、bをunreachableに配置
再帰:aは到達可能なので、ノードaから到達可能なすべてのノードを再帰的に到達可能なノードとしてマークします:
Clear: unreachableでクリアできるオブジェクトはないので、オブジェクトa,b,cはクリアされません。
まとめ
全体として、pythonはメモリプーリングによってメモリの断片化を減らし、実行効率を向上させています。ゴミ収集は主に参照カウントによって行われ、マークアンドクリーンはコンテナオブジェクトへの循環参照によって引き起こされる問題を解決し、世代回復はゴミ収集の効率を向上させます。