この記事を読むのに約5分かかります。
プログラムを一度実行した後に終了すると、メモリ管理の重要性に気付かない場合があります。作成するプログラムを7x24時間継続して実行する必要がある場合、メモリ管理は非常に重要です。特に重要なサービスでは、メモリリークが発生しないようにする必要があります。
ここでのメモリリークは、メモリ内のデータの損失やメモリスペースの物理的な消失によるものではなく、プログラム自体が適切に設計されていないため、占有されているメモリが解放されても実際には解放されないため、システムで使用可能なメモリが深刻になります。不十分な場合、これが原因でシステムまたはサービスがクラッシュします。
Pythonはどのようにガベージコレクションを実行しますか?言い換えれば、Pythonは使用されなくなったメモリスペースをどのように再利用するのでしょうか。
ご存知のとおり、Pythonのすべてがオブジェクトであり、オブジェクトは一定量のメモリを占有します。変数を介してオブジェクトにアクセスします。変数の本質は、オブジェクトのポインタ(アドレス)です。
どのオブジェクトスペースを再利用するかを決定する方法は、そのような方法を考えるのは簡単です。オブジェクトを指す変数がない場合、それは役に立たないことを意味し、オブジェクトが占めるスペースを再利用できます。実際、Pythonはこれを行います。コードの一部と、その結果を見てみましょう。
import os
import psutil
# 現在のpythonプログラムが占有しているメモリサイズを表示します
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_info()
memory = info.rss /1024.0/1024print("{}メモリフットプリント: {} MB".format(hint, memory))
def func():show_memory_info("funcが呼び出される前")
a =[i for i inrange(10000000)]show_memory_info("funcが呼び出される前")if __name__ =="__main__":func()show_memory_info("funcが呼び出された後")
結果は次のとおりです。
funcが呼び出される前のメモリ使用量:29.63671875 MB
func呼び出しが終了する前のメモリ使用量:414.1640625 MB
func呼び出し終了後のメモリ使用量:27.2265625 MB
この例では、関数funcで大きなリストaが作成された後、メモリ使用量がすぐに400 MBに増加し、func呼び出しの終了後にメモリが27 MBに復元されることがわかります。これは、func呼び出しの終了後、Pythonが変数aがもはや存在しないことを認識していることを示しています。使用するため、ガベージコレクションを行います。
別の見方をすれば、関数で宣言されたリストはローカル変数です。関数が戻った後、ローカル変数の参照はキャンセルされます。この時点で、リストaが指すオブジェクトへの参照の数は0であり、Pythonはガベージコレクションを実行します。 、したがって、以前に占有されていた大量のメモリが戻ってきました。
func関数の変数aをグローバル変数に変更すると、関数呼び出しが終了した後もaが使用され、この時点でメモリは再利用されません。
def func():show_memory_info("funcが呼び出される前")
global a
a =[i for i inrange(10000000)]show_memory_info("funcが呼び出される前")
実行結果は次のとおりです。
funcが呼び出される前のメモリ使用量:29.625 MB
func呼び出しが終了する前のメモリ使用量:416.796875 MB
func呼び出し終了後のメモリ使用量:416.80078125 MB
つまり、変数の参照カウントが0の場合、Pythonインタープリターはそれを増やして再利用できます。したがって、問題は、オブジェクトへの参照の数をどのように判断するかということです。
幸い、Python標準ライブラリには、変数参照カウントを直接表示できる関数 sys.getrefcount(var)
が用意されています。使用方法は以下のとおりです。
import sys
a =[]
# 2つの参照、1つはaから、もう1つはgetrefcountから
print(sys.getrefcount(a))
def func(a):
# 4つの参照、a、python関数呼び出しスタック、関数パラメーター、およびgetrefcount
print(sys.getrefcount(a))func(a)
# 2つの参照(1つはaから、もう1つはgetrefcountから)、関数func呼び出しは存在しなくなりました
print(sys.getrefcount(a))
出力
242
簡単な紹介として、sys.getrefcount()関数は、変数への参照の数を表示できます。このコード自体は理解しやすいはずですが、getrefcount自体もカウントを導入することを忘れないでください。
もう1つの注意点は、関数呼び出しが発生すると、2つの追加の参照があります。1つは関数スタックから、もう1つは関数パラメーターからです。
import sys
a =[]print(sys.getrefcount(a)) #2回
b = a
print(sys.getrefcount(a)) #3回
c = b
d = b
e = c
f = e
g = d
print(sys.getrefcount(a)) #8回
このコードを見ると、少し注意を払う必要があります。変数a、b、c、d、e、f、gはすべて同じオブジェクトを参照し、sys.getrefcount()関数はポインターをカウントしませんが、オブジェクトへの参照の数をカウントするため、最終的に合計8つの参照があります。
これで、Pythonが自動的にゴミを収集することがわかりました。
Pythonは自動的にメモリを再利用できますが、手動でメモリを再利用することをお勧めします。非常に簡単です。変数aがあり、それを再度使用したくない場合は、2つのコードを実行して取得します。
del a
gc.collect()
誰かがそれを理解していると思わなければならないと思います。そして、インタビュアーがこの時点で尋ねた場合:0引用は、ゴミ収集を開始するために必要かつ十分な条件ですか?他の可能性はありますか?
あなたも閉じ込められても心配しないでください。小さな手順を実行して、最初にこの質問について考えることもできます。相互に参照し、他のオブジェクトによって参照されなくなった2つのオブジェクトがある場合、それらはガベージコレクションする必要がありますか?
import os,sys
import psutil
# 現在のpythonプログラムが占有しているメモリサイズを表示します
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_info()
memory = info.rss /1024.0/1024print("{}メモリフットプリント: {} MB".format(hint, memory))
def func2():show_memory_info("func2が呼び出される前")
a =[i for i inrange(10000000)]
b =[i for i inrange(10000000)]
a.append(b)
b.append(a)show_memory_info("通話終了前のfunc2")if __name__ =="__main__":func2()show_memory_info("func2が呼び出された後")
出力は次のとおりです。
func2が呼び出される前のメモリ使用量:29.65625 MB
func2呼び出しが終了する前のメモリ使用量:795.01953125 MB
func2が呼び出された後のメモリ使用量:795.09375 MB
ここで、aとbは相互に参照し、ローカル変数として、関数funcが呼び出された後、2つのポインターaとbはプログラムの意味で存在しなくなります。ただし、まだメモリ使用量があることは明らかです。どうして?相互参照のため、参照の数は0ではありません。
このコードが実稼働環境に表示された場合、最初はaとbが占めるスペースがそれほど大きくなくても、長時間実行すると、Pythonが占めるメモリは確実にどんどん大きくなり、最終的にはバーストすることを想像してみてください。サーバー、結果は悲惨です。
もちろん、お互いを引用するのは簡単だと言う人もいるかもしれませんが、それは大きな問題ではありません。ただし、より隠された状況は、参照ループの出現です。複雑なエンジニアリングコードの場合、参照ループを簡単に発見できない可能性があります。
参照リングの発生を本当に恐れてチェックアウトできない場合は、 gc.collect()
を呼び出してゴミを収集し、上記のコードfunc2呼び出しの最後に gc.collect()
を呼び出すことができます。
if __name__ =="__main__":func2()
gc.collect()show_memory_info("func2が呼び出された後")
の結果
func2が呼び出される前のメモリ使用量:29.625 MB
func2呼び出しが終了する前のメモリ使用量:804.62109375 MB
func2が呼び出された後のメモリ使用量:30.54296875 MB
上記は手動コレクションのデモンストレーションです。実際、Pythonはそれを自動的に処理できます。Pythonはマークスイープアルゴリズムと世代別コレクションを使用して、循環参照の自動ガベージコレクションを有効にします。
最初にマークスイープアルゴリズムを見てみましょう。最初にグラフ理論を使用して、到達不能の概念を理解しましょう。有向グラフの場合、トラバーサルがノードから始まり、通過するすべてのノードがマークされている場合、トラバーサルが終了した後、マークされていないすべてのノードは到達不能ノードと呼ばれます。もちろん、これらのノードの存在は無意味ですが、当然、ゴミ収集する必要があります。もちろん、グラフ全体を毎回トラバースすることは、Pythonにとって大きなパフォーマンスの浪費です。したがって、Pythonのガベージコレクションの実装では、mark-sweepは二重にリンクされたリストを使用してデータ構造を維持し、コンテナオブジェクトのみを考慮します(コンテナオブジェクトのみが循環参照を生成できます)。ここでは特定のアルゴリズムについてはこれ以上説明しません。結局のところ、私たちの焦点はアプリケーションにあります。
世代別コレクションをもう一度見てみましょう。Pythonはすべてのオブジェクトを3つの世代に分割します。新しく作成されたオブジェクトは第0世代です。ガベージコレクションの後、まだ存在しているオブジェクトは前の世代から次の世代に順番に移動されます。世代ごとに自動ガベージコレクションを開始するためのしきい値は、個別に指定できます。ガベージコレクターで削除されたオブジェクトを差し引いた新しいオブジェクトが対応するしきい値に達すると、この世代のオブジェクトはガベージコレクションされます。実際、世代別収集は、新しいオブジェクトはゴミ収集される可能性が高く、存続期間が長いオブジェクトほど存続する可能性が高いという考えに基づいています。したがって、このアプローチにより、多くの計算を節約でき、それによってPythonのパフォーマンスが向上します。
あなたは今直面した質問に答えることができるはずです!はい、参照カウントは最も単純な実装の1つですが、参照カウントは必要かつ十分な条件ではなく、十分な非必須条件としてのみカウントできることを忘れないでください。他の可能性については、私たちが話している循環参照もその1つです。
上記のブレスレットリファレンスのように、ツリーのような図で変数のリファレンス関係を表す方法はありますか?したがって、メモリリークをデバッグできます。実際、objgraphと呼ばれる、参照関係を視覚化するための非常に便利なパッケージがあります。このパッケージでは、主に2つの関数をお勧めします。1つ目は、明確な参照図を生成できる show_refs()
です。
import objgraph
a =[1,2,3]
b =[4,5,6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
objgraph.show_backrefs([a])
これらは、実行後にわかる参照関係をグラフィカルに表示します。これは、デバッグに非常に便利です。公式文書https://mg.pov.lt/objgraph/。
1、 Pythonは自動的にガベージコレクションを実行します。 2.参照カウントが0の場合のリサイクルが最も単純なケースであり、循環参照があります。 3.Pythonには2つの自動リサイクルアルゴリズムがあります。
4、 メモリリークをデバッグするために、objgraphは優れた視覚分析ツールです。
(終了)
Recommended Posts