Python LanguageRaccolta dei rifiuti

Osservazioni

Al suo interno, il garbage collector di Python (a partire da 3.5) è una semplice implementazione di conteggio dei riferimenti. Ogni volta che si effettua un riferimento a un oggetto (ad esempio, a = myobject ) il conteggio dei riferimenti su quell'oggetto (oggetto mio) viene incrementato. Ogni volta che un riferimento viene rimosso, il conteggio dei riferimenti viene decrementato e una volta che il conteggio dei riferimenti raggiunge 0 , sappiamo che nulla contiene un riferimento a quell'oggetto e possiamo deallocarlo!

Un comune malinteso su come funziona la gestione della memoria di Python è che la parola chiave del libera la memoria degli oggetti. Questo non è vero. Quello che succede in realtà è che la parola chiave del decrementa semplicemente il conto degli oggetti, il che significa che se lo chiami abbastanza volte perché il refcount raggiunga lo zero l'oggetto può essere garbage collection (anche se in realtà ci sono ancora riferimenti all'oggetto disponibile altrove nel tuo codice ).

Python crea o pulisce aggressivamente gli oggetti la prima volta che ne ha bisogno Se eseguo il compito a = object (), la memoria per oggetto viene allocata in quel momento (a volte cpython riutilizzerà determinati tipi di oggetti, ad esempio liste sotto il cofano, ma per lo più non mantiene un pool di oggetti gratuito e eseguirà l'allocazione quando ne hai bisogno). Allo stesso modo, non appena il conto è decrementato a 0, GC lo pulisce.

Raccolta di rifiuti generazionale

Negli anni '60 John McCarthy scoprì un difetto fatale nel conteggiare la raccolta dei rifiuti quando implementò l'algoritmo di refcount utilizzato da Lisp: cosa succede se due oggetti si riferiscono l'un l'altro in un riferimento ciclico? Come puoi mai raccogliere i rifiuti di quei due oggetti, anche se non ci sono riferimenti esterni a loro se si riferiscono sempre a vicenda? Questo problema si estende anche a qualsiasi struttura ciclica dei dati, ad esempio un buffer circolare o due voci consecutive in una lista doppiamente collegata. Python tenta di risolvere questo problema usando una svolta leggermente interessante su un altro algoritmo di garbage collection chiamato Generational Garbage Collection .

In pratica, ogni volta che crei un oggetto in Python lo aggiunge alla fine di una lista doppiamente collegata. A volte Python scorre questo elenco, controlla quali oggetti si riferiscono anche agli oggetti nella lista, e se sono anche nella lista (vedremo perché potrebbero non essere in un momento), decrementa ulteriormente i loro conti. A questo punto (in realtà, ci sono alcune euristiche che determinano quando le cose vengono spostate, ma supponiamo che sia dopo una singola raccolta per mantenere le cose semplici) tutto ciò che ha ancora un conto maggiore di 0 viene promosso ad un'altra lista collegata chiamata "Generazione 1" (questo è il motivo per cui tutti gli oggetti non sono sempre nella lista di generazione 0) a cui questo ciclo si applica meno spesso. È qui che arriva la garbage collection generazionale. In Python ci sono 3 generazioni predefinite (tre liste di oggetti collegate): la prima lista (generazione 0) contiene tutti i nuovi oggetti; se si verifica un ciclo GC e gli oggetti non vengono raccolti, vengono spostati nella seconda lista (generazione 1) e se un ciclo GC si verifica nella seconda lista e non vengono ancora raccolti vengono spostati nella terza lista (generazione 2 ). L'elenco di terza generazione (chiamato "generazione 2", dato che siamo indicizzati a zero) è garbage collection molto meno spesso dei primi due, l'idea è che se il tuo oggetto è longevo non è probabile che sia GCed, e non potrà mai essere GCed durante la vita della tua applicazione, quindi non c'è motivo di perdere tempo a controllarlo su ogni singola esecuzione di GC. Inoltre, si osserva che la maggior parte degli oggetti viene raccolta in modo relativamente rapido. D'ora in poi, chiameremo questi "oggetti buoni" poiché muoiono giovani. Questa è chiamata "debole ipotesi generazionale" e fu anche osservata per la prima volta negli anni '60.

Un rapido accostamento: a differenza delle prime due generazioni, la lunga lista di terza generazione non è raccolta di rifiuti su base regolare. Viene verificato quando il rapporto tra gli oggetti in sospeso di lunga durata (quelli che si trovano nell'elenco di terza generazione, ma non hanno ancora avuto un ciclo GC) per gli oggetti long life totali nell'elenco è superiore al 25%. Questo perché la terza lista è illimitata (le cose non vengono mai spostate da essa in un'altra lista, quindi vanno via solo quando sono effettivamente raccolte dalla spazzatura), il che significa che per le applicazioni in cui si stanno creando molti oggetti longevi, cicli GC sulla terza lista può diventare piuttosto lungo. Usando un rapporto otteniamo "prestazioni lineari ammortizzate nel numero totale di oggetti"; alias, più lunga è la lista, più GC è lungo, ma meno spesso eseguiamo GC (ecco la proposta originale del 2008 per questa euristica di Martin von Löwis per la lettura successiva). L'atto di eseguire una garbage collection sulla terza generazione o "matura" è chiamato "full garbage collection".

Quindi la raccolta di dati generazionali accelera tremendamente le cose non richiedendo di eseguire la scansione su oggetti che non hanno probabilmente bisogno di GC tutto il tempo, ma come ci aiutano a interrompere i riferimenti ciclici? Probabilmente non molto bene, si scopre. La funzione per interrompere effettivamente questi cicli di riferimento inizia così :

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)

Il motivo per cui la garbage collection generazionale aiuta è che possiamo mantenere la lunghezza della lista come un conteggio separato; ogni volta che aggiungiamo un nuovo oggetto alla generazione incrementiamo questo conteggio, e ogni volta che spostiamo un oggetto su un'altra generazione o dealloc decrementiamo il conteggio. Teoricamente alla fine di un ciclo di GC questo conteggio (sempre per le prime due generazioni) dovrebbe sempre essere 0. Se non lo è, qualsiasi cosa nella lista rimasta è una qualche forma di riferimento circolare e possiamo lasciarla cadere. Tuttavia, c'è un altro problema qui: Cosa succede se gli oggetti __del__ il metodo magico di Python __del__ su di essi? __del__ è chiamato ogni volta che un oggetto Python viene distrutto. Tuttavia, se due oggetti in un riferimento circolare hanno metodi __del__ , non possiamo essere sicuri che la distruzione di uno non interromperà il metodo __del__ degli altri. Per un esempio forzato, immagina di aver scritto quanto segue:

class A(object):
    def __init__(self, b=None):
        self.b = b
 
    def __del__(self):
        print("We're deleting an instance of A containing:", self.b)
     
class B(object):
    def __init__(self, a=None):
        self.a = a
 
    def __del__(self):
        print("We're deleting an instance of B containing:", self.a)

e impostiamo un'istanza di A e un'istanza di B per puntare l'un l'altro e quindi finiscono nello stesso ciclo di raccolta dei rifiuti? Diciamo che ne prendiamo uno a caso e deallociamo la nostra istanza di A prima; Verrà chiamato il metodo __del__ di A, verrà stampato, quindi A verrà liberato. Quindi arriviamo a B, chiamiamo il suo metodo __del__ e oops! Segfault! A non esiste più. Potremmo risolvere questo problema chiamando tutto ciò che è rimasto prima i metodi __del__ , poi facendo un altro passaggio per dealloc effettivamente tutto, tuttavia, questo introduce un altro problema: cosa succede se un oggetto __del__ metodo salva un riferimento all'altro oggetto che sta per essere GCed e ci ha fatto riferimento da qualche altra parte? Abbiamo ancora un ciclo di riferimento, ma ora non è possibile eseguire effettivamente GC su un oggetto, anche se non sono più in uso. Si noti che anche se un oggetto non fa parte di una struttura di dati circolare, potrebbe rianimarsi nel proprio metodo __del__ ; Python ha un controllo per questo e interromperà GCing se un account di oggetti è aumentato dopo che è stato chiamato il suo metodo __del__ .

CPython si occupa di ciò bloccando quegli oggetti non-GC (qualsiasi cosa con qualche forma di riferimento circolare e un metodo __del__ ) su una lista globale di rifiuti non recuperabili e lasciandoli lì per l'eternità:

/* list of uncollectable objects */
static PyObject *garbage = NULL;

Raccolta dei rifiuti Esempi correlati