Ricerca…


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;

Conteggio di riferimento

La maggior parte della gestione della memoria di Python viene gestita con il conteggio dei riferimenti.

Ogni volta che un oggetto viene referenziato (ad es. Assegnato a una variabile), il suo conteggio dei riferimenti viene automaticamente aumentato. Quando è dereferenziato (ad es. La variabile esce dal campo di applicazione), il suo conteggio di riferimento viene automaticamente diminuito.

Quando il conteggio dei riferimenti raggiunge lo zero, l'oggetto viene immediatamente distrutto e la memoria viene immediatamente liberata. Pertanto, per la maggior parte dei casi, il garbage collector non è nemmeno necessario.

>>> import gc; gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> def foo():
        Track()
        # destructed immediately since no longer has any references
        print("---")
        t = Track()
        # variable is referenced, so it's not destructed yet
        print("---")
        # variable is destructed when function exits
>>> foo()
Initialized
Destructed
---
Initialized
---
Destructed

Per dimostrare ulteriormente il concetto di riferimenti:

>>> def bar():
        return Track()
>>> t = bar()
Initialized
>>> another_t = t  # assign another reference
>>> print("...")
...
>>> t = None          # not destructed yet - another_t still refers to it
>>> another_t = None  # final reference gone, object is destructed
Destructed

Garbage Collector per cicli di riferimento

L'unica volta che è necessario il garbage collector è se si dispone di un ciclo di riferimento . L'esempio semplice di un ciclo di riferimento è uno in cui A si riferisce a B e B si riferisce ad A, mentre nient'altro si riferisce ad A o B. Né A né B sono accessibili da qualsiasi parte del programma, quindi possono essere distrutti in modo sicuro, tuttavia i loro conteggi di riferimento sono 1 e quindi non possono essere liberati solo dall'algoritmo di conteggio di riferimento.

>>> import gc; gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> A = Track()
Initialized
>>> B = Track()
Initialized
>>> A.other = B
>>> B.other = A
>>> del A; del B  # objects are not destructed due to reference cycle
>>> gc.collect()  # trigger collection
Destructed
Destructed
4

Un ciclo di riferimento può essere arbitrario. Se A punta a B punta a C punta a ... punta a Z che punta ad A, quindi non verranno collezionate da A a Z, fino alla fase di garbage collection:

>>> objs = [Track() for _ in range(10)]
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
>>> for i in range(len(objs)-1):
...     objs[i].other = objs[i + 1]
...
>>> objs[-1].other = objs[0]  # complete the cycle
>>> del objs                  # no one can refer to objs now - still not destructed
>>> gc.collect()
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
20

Effetti del comando

Rimozione di un nome di variabile dall'ambito usando del v , o rimozione di un oggetto da una raccolta usando del v[item] o del[i:j] , o rimuovendo un attributo usando del v.name , o qualsiasi altro modo di rimuovere riferimenti a un oggetto, non innesca alcuna chiamata distruttore o alcuna memoria liberata in sé e per sé. Gli oggetti vengono distrutti solo quando il loro conteggio dei riferimenti raggiunge lo zero.

>>> import gc
>>> gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> def bar():
    return Track()
>>> t = bar()
Initialized
>>> another_t = t  # assign another reference
>>> print("...")
...
>>> del t          # not destructed yet - another_t still refers to it
>>> del another_t  # final reference gone, object is destructed
Destructed

Riutilizzo di oggetti primitivi

Una cosa interessante da notare, che può aiutare a ottimizzare le tue applicazioni, è che anche le primitive vengono effettivamente riconquistate. Diamo un'occhiata ai numeri; per tutti gli interi compresi tra -5 e 256, Python riutilizza sempre lo stesso oggetto:

>>> import sys
>>> sys.getrefcount(1)
797
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
799

Si noti che il Refcount aumenta, il che significa che a e b riferimento allo stesso oggetto sottostante quando si riferiscono alla primitiva 1 . Tuttavia, per numeri più grandi, Python in realtà non riutilizza l'oggetto sottostante:

>>> a = 999999999
>>> sys.getrefcount(999999999)
3
>>> b = 999999999
>>> sys.getrefcount(999999999)
3

Poiché il refcount per 999999999 non cambia durante l'assegnazione ad a e b possiamo dedurre che si riferiscono a due diversi oggetti sottostanti, anche se entrambi viene assegnato lo stesso primitivo.

Visualizzazione del conto di un oggetto

>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2

Disabilitare forzatamente gli oggetti

Puoi forzare gli oggetti deallocate anche se il loro refcount non è 0 in entrambi Python 2 e 3.

Entrambe le versioni usano il modulo ctypes per farlo.

ATTENZIONE: facendo questo lascerà il vostro ambiente Python instabile e soggetto a crash senza un traceback! L'utilizzo di questo metodo potrebbe anche introdurre problemi di sicurezza (alquanto improbabili) Distribuisci solo gli oggetti di cui sei sicuro di non fare mai più riferimento. Mai.

Python 3.x 3.0
import ctypes
deallocated = 12345
ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
Python 2.x 2.3
import ctypes, sys
deallocated = 12345
(ctypes.c_char * sys.getsizeof(deallocated)).from_address(id(deallocated))[:4] = '\x00' * 4

Dopo l'esecuzione, qualsiasi riferimento all'oggetto now deallocated farà sì che Python produca un comportamento non definito o crash - senza un traceback. C'era probabilmente una ragione per cui il garbage collector non ha rimosso quell'oggetto ...

Se deallocate None , viene visualizzato un messaggio speciale Fatal Python error: deallocating None prima dell'arresto anomalo.

Gestire la garbage collection

Esistono due approcci per influenzare quando viene eseguita una pulizia della memoria. Stanno influenzando la frequenza con cui viene eseguito il processo automatico e l'altro sta attivando manualmente una pulizia.

Il garbage collector può essere manipolato sintonizzando le soglie di raccolta che influiscono sulla frequenza di esecuzione del collector. Python utilizza un sistema di gestione della memoria basato sulla generazione. I nuovi oggetti vengono salvati nella nuova generazione - generazione0 e con ciascuna raccolta sopravvissuta, gli oggetti vengono promossi alle generazioni precedenti. Dopo aver raggiunto l'ultima generazione - generazione2 , non vengono più promossi.

Le soglie possono essere modificate utilizzando il seguente snippet:

import gc
gc.set_threshold(1000, 100, 10) # Values are just for demonstration purpose

Il primo argomento rappresenta la soglia per la raccolta di generazione0 . Ogni volta che il numero di allocazioni supera il numero di deallocazioni per 1000, verrà chiamato il garbage collector.

Le generazioni precedenti non vengono pulite ad ogni analisi per ottimizzare il processo. Il secondo e il terzo argomento sono facoltativi e controllano la frequenza con cui vengono pulite le generazioni precedenti. Se la generazione0 è stata elaborata 100 volte senza eseguire la pulizia di generation1 , verrà generata la generazione1 . Allo stesso modo, gli oggetti in generation2 verranno elaborati solo quando quelli in generation1 sono stati puliti 10 volte senza toccare generation2 .

Un'istanza in cui l'impostazione manuale delle soglie è vantaggiosa è quando il programma assegna un sacco di piccoli oggetti senza deallocarli che conduce troppo spesso al garbage collector (ogni allocazione di oggetto generation0_threshold ). Anche se il collezionista è piuttosto veloce, quando gira su un numero enorme di oggetti pone un problema di prestazioni. In ogni caso, non esiste una taglia adatta a tutte le strategie per la scelta delle soglie e il suo caso d'uso è affidabile.

L'attivazione manuale di una raccolta può essere eseguita come nel seguente snippet:

import gc
gc.collect()

La garbage collection viene automaticamente attivata in base al numero di allocazioni e deallocazioni, non sulla memoria consumata o disponibile. Di conseguenza, quando si lavora con oggetti di grandi dimensioni, la memoria potrebbe esaurirsi prima che venga avviata la pulizia automatica. Questo è un buon caso d'uso per chiamare manualmente il garbage collector.

Anche se è possibile, non è una pratica incoraggiata. Evitare perdite di memoria è l'opzione migliore. Ad ogni modo, nei grandi progetti l'individuazione della perdita di memoria può essere un compito ben preciso e l'attivazione manuale di una garbage collection può essere utilizzata come soluzione rapida fino a un ulteriore debugging.

Per i programmi con esecuzione prolungata, la garbage collection può essere attivata su base temporale o su base evento. Un esempio per il primo è un server Web che attiva una raccolta dopo un numero fisso di richieste. Per il futuro, un server Web che attiva una garbage collection quando viene ricevuto un determinato tipo di richiesta.

Non aspettare che la garbage collection sia pulita

Il fatto che la raccolta dei dati inutili venga eliminata non significa che è necessario attendere il ciclo di raccolta dei dati inutili per eseguire la pulizia.

In particolare, non è necessario attendere la garbage collection per chiudere handle di file, connessioni database e connessioni di rete aperte.

per esempio:

Nel seguente codice, si presuppone che il file verrà chiuso nel ciclo di garbage collection successivo, se f è l'ultimo riferimento al file.

>>> f = open("test.txt")
>>> del f

Un modo più esplicito per ripulire è chiamare f.close() . Puoi farlo ancora più elegante, utilizzando l'istruzione with , anche conosciuta come context manager :

>>> with open("test.txt") as f:
...     pass
...     # do something with f
>>> #now the f object still exists, but it is closed

L'istruzione with consente di indentare il codice sotto il file aperto. Ciò rende esplicito e più facile vedere per quanto tempo un file viene tenuto aperto. Inoltre, chiude sempre un file, anche se viene sollevata un'eccezione nel blocco while .



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow