Come funziona Java Garbage Collection con i riferimenti circolari?


161

Da quanto ho capito, la garbage collection in Java pulisce alcuni oggetti se nient'altro 'punta' a quell'oggetto.

La mia domanda è: cosa succede se abbiamo qualcosa del genere:

class Node {
    public object value;
    public Node next;
    public Node(object o, Node n) { value = 0; next = n;}
}

//...some code
{
    Node a = new Node("a", null), 
         b = new Node("b", a), 
         c = new Node("c", b);
    a.next = c;
} //end of scope
//...other code

a, be cdovrebbero essere raccolti in modo inutile, ma a tutti fanno riferimento altri oggetti.

In che modo la raccolta di rifiuti Java si occupa di questo? (o è semplicemente una perdita di memoria?)


1
Vedi: stackoverflow.com/questions/407855/… , in particolare la seconda risposta di @gnud.
Seth,

Risposte:


161

Il GC di Java considera gli oggetti "immondizia" se non sono raggiungibili attraverso una catena che inizia da una radice di Garbage Collection, quindi questi oggetti verranno raccolti. Anche se gli oggetti possono puntare l'uno verso l'altro per formare un ciclo, sono comunque spazzatura se tagliati dalla radice.

Vedi la sezione sugli oggetti irraggiungibili nell'Appendice A: La verità sulla raccolta dei rifiuti nelle prestazioni della piattaforma Java: strategie e tattiche per i dettagli cruenti.


14
Hai un riferimento per questo? È difficile testarlo.
Tangens,

5
Ho aggiunto un riferimento. Puoi anche ignorare il metodo finalize () di un oggetto per scoprire quando viene raccolto (anche se si tratta dell'unica cosa che consiglierei di usare finalize () per).
Bill the Lizard,

1
Giusto per chiarire quell'ultimo commento ... inserire un'istruzione di stampa di debug nel metodo finalize che stampa un ID univoco per l'oggetto. Sarai in grado di vedere tutti gli oggetti che fanno riferimento l'un l'altro vengono raccolti.
Bill the Lizard,

4
"... abbastanza intelligente da riconoscere ..." sembra confuso. GC non deve riconoscere i cicli - sono solo irraggiungibili, quindi immondizia
Alexander Malakhov

86
@tangens "Hai un riferimento per quello?" in una discussione sulla raccolta dei rifiuti. Migliore. Gioco di parole. Mai.
Michał Kosmulski,

139

sì Il Garbage Collector di Java gestisce riferimenti circolari!

How?

Esistono oggetti speciali chiamati radici della garbage collection (radici GC). Questi sono sempre raggiungibili, così come qualsiasi oggetto che li abbia alla propria radice.

Una semplice applicazione Java ha le seguenti radici GC:

  1. Variabili locali nel metodo principale
  2. Il filo conduttore
  3. Variabili statiche della classe principale

inserisci qui la descrizione dell'immagine

Per determinare quali oggetti non sono più in uso, la JVM esegue in modo intermittente quello che è molto giustamente chiamato algoritmo mark-and-sweep . Funziona come segue

  1. L'algoritmo attraversa tutti i riferimenti agli oggetti, a partire dalle radici GC, e segna ogni oggetto trovato come vivo.
  2. Tutta la memoria dell'heap che non è occupata da oggetti contrassegnati viene recuperata. È semplicemente contrassegnato come libero, essenzialmente spazzato via da oggetti inutilizzati.

Quindi, se un oggetto non è raggiungibile dalle radici GC (anche se è autoreferenziato o referenziato ciclicamente) sarà sottoposto alla garbage collection.

Naturalmente a volte questo può portare alla perdita di memoria se il programmatore dimentica di dereferenziare un oggetto.

inserisci qui la descrizione dell'immagine

Fonte: Java Memory Management


3
Spiegazione perfetta! Grazie! :)
Jovan Perovic,

Grazie per aver collegato quel libro. È pieno di ottime informazioni su questo e altri argomenti di sviluppo Java!
Droj,

14
Nell'ultima immagine, c'è un oggetto non raggiungibile ma è nella sezione oggetti raggiungibili.
La VloZ Merrill,

13

Un garbage collector parte da un insieme di posizioni "root" che sono sempre considerate "raggiungibili", come i registri della CPU, lo stack e le variabili globali. Funziona trovando qualsiasi puntatore in quelle aree e trovando ricorsivamente tutto ciò a cui puntano. Una volta trovato tutto ciò, tutto il resto è spazzatura.

Ci sono, ovviamente, alcune varianti, soprattutto per motivi di velocità. Ad esempio, la maggior parte dei moderni garbage collector sono "generazionali", nel senso che dividono gli oggetti in generazioni e, man mano che un oggetto invecchia, il garbage collector va sempre più a lungo tra le volte in cui cerca di capire se quell'oggetto è ancora valido o meno - inizia a presumere che se ha vissuto a lungo, è molto probabile che continuerà a vivere ancora più a lungo.

Tuttavia, l'idea di base rimane la stessa: è tutto basato sul partire da un insieme di cose che dà per scontato che potrebbe ancora essere usato, e quindi inseguire tutti i suggerimenti per trovare cos'altro potrebbe essere in uso.

Interessante a parte: che le persone siano spesso sorprese dal grado di somiglianza tra questa parte di un garbage collector e il codice per il marshalling di oggetti per cose come le chiamate di procedure remote. In ogni caso, stai partendo da un insieme di oggetti radice e insegui i puntatori per trovare tutti gli altri oggetti a cui si riferiscono ...


Quello che stai descrivendo è un collezionista di tracce. Esistono altri tipi di collezionisti. Di particolare interesse per questa discussione sono collettori di conteggio di riferimento, che non tendono ad avere problemi con cicli.
Jörg W Mittag,

@ Jörg W Mittag: Sicuramente vero - anche se non conosco una JVM (ragionevolmente attuale) che utilizza il conteggio dei riferimenti, quindi sembra improbabile (almeno per me) che faccia molta differenza rispetto alla domanda originale.
Jerry Coffin,

@ Jörg W Mittag: almeno per impostazione predefinita, credo che Jikes RVM utilizzi attualmente il raccoglitore Immix, che è un raccoglitore di tracce basato sulla regione (sebbene utilizzi anche il conteggio dei riferimenti). Non sono sicuro se ti riferisci a quel conteggio dei riferimenti o a un altro raccoglitore che utilizza il conteggio dei riferimenti senza traccia (indovino quest'ultimo, poiché non ho mai sentito parlare di Immix che chiama "riciclatore").
Jerry Coffin,

Mi sono confuso un po ': il Recycler è (era?) Implementato in Jalapeno, l'algoritmo a cui stavo pensando, che è (era?) Implementato in Jikes è Ulterior Reference Counting . Atlhough, ovviamente, dicendo che Jikes usa questo o quel garbage collector è abbastanza inutile, dato che Jikes e specialmente MMtk sono specificamente progettati per sviluppare rapidamente e testare diversi garbage collector all'interno della stessa JVM.
Jörg W Mittag,

2
Ulterior Reference Counting è stato progettato nel 2003 dalle stesse persone che hanno progettato Immix nel 2007, quindi immagino che quest'ultimo abbia probabilmente sostituito il primo. URC è stato appositamente progettato in modo da poter essere combinato con altre strategie, e in effetti il ​​documento URC menziona esplicitamente che URC è solo un trampolino di lancio verso un collezionista che combina i vantaggi della tracciabilità e del conteggio dei riferimenti. Immix è quel collezionista. Ad ogni modo, il Recycler è un puro collezionista di conteggio di riferimento, che può comunque rilevare e raccogliere cicli: WWW.Research.IBM.Com/people/d/dfb/recycler.html
Jörg W Mittag

13

Hai ragione. La forma specifica di garbage collection che descrivi si chiama " conteggio dei riferimenti ". Il modo in cui funziona (concettualmente, almeno, le implementazioni più moderne del conteggio dei riferimenti sono effettivamente implementate in modo abbastanza diverso) nel caso più semplice, si presenta così:

  • ogni volta che viene aggiunto un riferimento a un oggetto (ad esempio, viene assegnato a una variabile o a un campo, passato al metodo e così via), il suo conteggio dei riferimenti viene aumentato di 1
  • ogni volta che viene rimosso un riferimento a un oggetto (il metodo ritorna, la variabile esce dal campo di applicazione, il campo viene riassegnato a un altro oggetto o l'oggetto che contiene il campo si raccoglie spazzatura), il conteggio dei riferimenti viene ridotto di 1
  • non appena il conteggio dei riferimenti raggiunge 0, non c'è più riferimento all'oggetto, il che significa che nessuno può più usarlo, quindi è spazzatura e può essere raccolto

E questa semplice strategia ha esattamente il problema che decidi: se A fa riferimento a B e fa riferimento a A, entrambi i loro conteggi di riferimento non possono mai essere inferiori a 1, il che significa che non verranno mai raccolti.

Esistono quattro modi per affrontare questo problema:

  1. Ignoralo. Se hai abbastanza memoria, i tuoi cicli sono piccoli e rari e il tuo tempo di esecuzione è breve, forse puoi cavartela semplicemente non raccogliendo cicli. Pensa a un interprete di script di shell: gli script di shell in genere vengono eseguiti solo per pochi secondi e non allocano molta memoria.
  2. Combina il tuo riferimento contando il Garbage Collector con un altro Garbage Collector che non ha problemi con i cicli. CPython lo fa, ad esempio: il garbage collector principale in CPython è un collector di conteggio di riferimento, ma di tanto in tanto viene eseguito un garbage collector di traccia per raccogliere i cicli.
  3. Rileva i cicli. Sfortunatamente, il rilevamento di cicli in un grafico è un'operazione piuttosto costosa. In particolare, richiede praticamente lo stesso overhead di un collezionista di tracciamento, quindi potresti anche usare uno di quelli.
  4. Non implementare l'algoritmo nel modo ingenuo che io e te faremmo: dagli anni '70, sono stati sviluppati più algoritmi piuttosto interessanti che combinano il rilevamento del ciclo e il conteggio dei riferimenti in una singola operazione in un modo intelligente che è significativamente più economico di entrambi sia separatamente che facendo un collezionista di tracce.

A proposito, l' altro modo principale per implementare un garbage collector (e ho già accennato a questo un paio di volte sopra), è la traccia . Un collezionista di tracce si basa sul concetto di raggiungibilità . Si inizia con alcuni set di root che si sa siano sempre raggiungibili (costanti globali, ad esempio, o la Objectclasse, l'ambito lessicale corrente, il frame dello stack corrente) e da lì si tracciano tutti gli oggetti raggiungibili dal set di root, quindi tutti gli oggetti che sono raggiungibili dagli oggetti raggiungibili dal set di root e così via, fino a quando non si ha la chiusura transitiva. Tutto ciò che non si trova in quella chiusura è spazzatura.

Poiché un ciclo è raggiungibile solo all'interno di se stesso, ma non raggiungibile dal set di root, verrà raccolto.


1
Poiché la domanda è specifica di Java, penso che valga la pena ricordare che Java non utilizza il conteggio dei riferimenti e quindi il problema inesistente. Anche il collegamento a Wikipedia sarebbe utile come "ulteriore lettura". Altrimenti ottima panoramica!
Alexander Malakhov,

Ho appena letto i tuoi commenti al post di Jerry Coffin, quindi ora non ne sono sicuro :)
Alexander Malakhov,

8

I GC Java non si comportano effettivamente come descritto. È più preciso affermare che partono da un insieme di oggetti di base, spesso chiamati "radici GC", e raccolgono qualsiasi oggetto che non può essere raggiunto da una radice.
Le radici GC includono cose come:

  • variabili statiche
  • variabili locali (inclusi tutti i riferimenti 'questo' applicabili) attualmente nello stack di un thread in esecuzione

Quindi, nel tuo caso, una volta che le variabili locali a, b e c escono dall'ambito alla fine del tuo metodo, non ci sono più radici GC che contengono, direttamente o indirettamente, un riferimento a uno dei tuoi tre nodi, e saranno idonei per la raccolta dei rifiuti.

Il link di TofuBeer ha più dettagli se lo desideri.


"... attualmente nello stack di un thread in esecuzione ..." non sta eseguendo la scansione di stack di tutti i thread per non corrompere i dati di altri thread?
Alexander Malakhov,

6

Questo articolo (non più disponibile) approfondisce il raccoglitore di rifiuti (concettualmente ... ci sono diverse implementazioni). La parte rilevante per il tuo post è "A.3.4 Non raggiungibile":

A.3.4 Non raggiungibile Un oggetto entra in uno stato non raggiungibile quando non esistono più riferimenti forti ad esso. Quando un oggetto non è raggiungibile, è un candidato per la raccolta. Nota la formulazione: solo perché un oggetto è un candidato per la raccolta non significa che verrà immediatamente raccolto. La JVM è libera di ritardare la raccolta fino a quando non è immediatamente necessario che la memoria venga consumata dall'oggetto.



1
i collegamenti non sono più disponibili
titus

1

La garbage collection di solito non significa "ripulisci un oggetto se nient'altro" punta "a quell'oggetto" (che è il conteggio dei riferimenti). Garbage Collection significa approssimativamente trovare oggetti che non possono essere raggiunti dal programma.

Quindi, nel tuo esempio, dopo che a, b e c non rientrano nell'ambito, possono essere raccolti dal GC, poiché non è più possibile accedere a questi oggetti.


"La garbage collection significa approssimativamente trovare oggetti che non possono essere raggiunti dal programma". Nella maggior parte degli algoritmi GC è in realtà il contrario. Inizi con le radici GC e vedi cosa riesci a trovare, il resto è considerato spazzatura senza riferimento.
Fredrik,

1
Il conteggio dei riferimenti è una delle due principali strategie di implementazione per la garbage collection. (L'altro sta tracciando.)
Jörg W Mittag,

3
@Jörg: La maggior parte delle volte, oggi, quando le persone parlano di netturbini si riferiscono a collezionisti basati su una sorta di algoritmo mark'n'sweep. Il conteggio dei ref è in genere quello con cui sei bloccato se non hai un garbage collector. È vero che il conteggio dei ref è in un certo senso una strategia di garbage collection, ma oggi non esiste quasi nessun gc che si basa su di esso, quindi dire che è una strategia di gc confonderà le persone perché in pratica non è più un gc strategia ma un modo alternativo per gestire la memoria.
Fredrik,

1

Bill ha risposto direttamente alla tua domanda. Come ha detto Amnon, la tua definizione di garbage collection è solo il conteggio dei riferimenti. Volevo solo aggiungere che anche algoritmi molto semplici come mark, sweep e copy collection gestiscono facilmente riferimenti circolari. Quindi, niente di magico al riguardo!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.