Perché Garbage Collection spazza solo l'heap?


28

Fondamentalmente, ho imparato finora che la garbage collection cancella per sempre qualsiasi struttura di dati che non è attualmente indicata. Ma questo controlla solo l'heap per tali condizioni.

Perché non controlla anche la sezione dati (globali, costanti, ecc.) O lo stack? Cosa c'è nell'heap che è l'unica cosa che vogliamo che venga raccolta?


21
"spazzare il mucchio" è più sicuro di "colpire lo stack" ... :-)
Brian Knoblauch,

Risposte:


62

Il garbage collector esegue la scansione dello stack - per vedere quali oggetti nell'heap sono attualmente utilizzati (indicati) dagli oggetti nello stack.

Non ha senso che il garbage collector consideri la raccolta della memoria dello stack perché lo stack non è gestito in questo modo: tutto sullo stack è considerato "in uso". E la memoria utilizzata dallo stack viene automaticamente recuperata quando si ritorna dalle chiamate di metodo. La gestione della memoria dello spazio dello stack è così semplice, economica e facile che non si vorrebbe coinvolgere la garbage collection.

(Esistono sistemi, come smalltalk, in cui i frame dello stack sono oggetti di prima classe archiviati nell'heap e nei rifiuti raccolti come tutti gli altri oggetti. Ma questo non è l'approccio popolare in questi giorni. JVM di Java e CLR di Microsoft usano lo stack hardware e la memoria contigua .)


7
+1 lo stack è sempre completamente raggiungibile, quindi non ha senso spazzarlo
maniaco del cricchetto

2
+1 grazie, ho preso 4 messaggi per trovare la risposta giusta. Non so perché dovessi dire che tutto nello stack è "considerato" come in uso, è in uso almeno tanto forte quanto gli oggetti di heap ancora in uso sono in uso - ma questo è un vero pazzo di un'ottima risposta.
psr

@psr significa che tutto nello stack è fortemente raggiungibile e non ha bisogno di essere raccolto fino a quando il metodo non ritorna ma che (RAII) è già esplicitamente gestito
maniaco del cricchetto

@ratchetfreak - Lo so. E volevo solo dire che la parola "considerato" probabilmente non è necessaria, va bene fare una dichiarazione più forte senza di essa.
psr

5
@psr: non sono d'accordo. " considerato in uso" è più corretto sia per stack che per heap, per motivi molto importanti. Quello che vuoi è scartare ciò che non sarà più usato; quello che fai è che scarti ciò che non è raggiungibile . Potresti avere dati raggiungibili che non ti serviranno mai; quando questi dati crescono, hai una perdita di memoria (sì, sono possibili anche nei linguaggi GC, a differenza di molte persone pensano). E si potrebbe obiettare che si verificano anche perdite dello stack, l'esempio più comune è rappresentato dai frame stack non necessari nei programmi ricorsivi di coda eseguiti senza eliminazione delle chiamate di coda (ad esempio su JVM).
Blaisorblade,

19

Trasforma la tua domanda in giro. La vera domanda motivante è in quali circostanze possiamo evitare i costi della raccolta dei rifiuti?

Bene, prima di tutto, quali sono i costi della raccolta dei rifiuti? Ci sono due costi principali. Innanzitutto, devi determinare cosa è vivo ; ciò richiede potenzialmente molto lavoro. Secondo, devi compattare i buchi che si formano quando liberi qualcosa che è stato allocato tra due cose che sono ancora vive. Quei buchi sono dispendiosi. Ma compattarli è anche costoso.

Come possiamo evitare questi costi?

Chiaramente se riesci a trovare un modello di utilizzo dello storage in cui non allochi mai qualcosa di lunga durata, quindi alloca qualcosa di breve durata, quindi alloca qualcosa di lunga durata, puoi eliminare il costo dei buchi. Se puoi garantire che per alcuni sottogruppi della tua memoria, ogni allocazione successiva ha una vita più breve della precedente in quella memoria, allora non ci saranno mai buchi in quella memoria.

Ma se abbiamo risolto il problema del buco , abbiamo risolto anche il problema della raccolta dei rifiuti . Hai qualcosa in quel deposito ancora vivo? Sì. Tutto è stato assegnato prima che durasse più a lungo? Sì, questo presupposto è come abbiamo eliminato la possibilità di buchi. Pertanto è sufficiente dire "è attiva l'allocazione più recente?" e sai che tutto è vivo in quella memoria.

Abbiamo una serie di allocazioni di archiviazione in cui sappiamo che ogni allocazione successiva ha una durata inferiore rispetto all'allocazione precedente? Sì! I frame di metodi di attivazione vengono sempre distrutti nell'ordine opposto rispetto a quando sono stati creati perché hanno sempre una vita più breve rispetto all'attivazione che li ha creati.

Pertanto, possiamo archiviare i frame di attivazione nello stack e sapere che non devono mai essere raccolti. Se nella pila è presente un fotogramma, l'intera serie di fotogrammi sottostanti ha una durata maggiore, quindi non è necessario che vengano raccolti. E saranno distrutti nell'ordine opposto rispetto alla loro creazione. Il costo della raccolta dei rifiuti viene quindi eliminato per i frame di attivazione.

Ecco perché abbiamo il pool temporaneo nello stack in primo luogo: perché è un modo semplice di implementare l'attivazione del metodo senza incorrere in una penalità di gestione della memoria.

(Ovviamente il costo della spazzatura che raccoglie la memoria a cui fanno riferimento riferimenti nei frame di attivazione è ancora lì.)

Consideriamo ora un sistema di flusso di controllo in cui i frame di attivazione non vengono distrutti in un ordine prevedibile. Cosa succede se un'attivazione di breve durata può generare un'attivazione di lunga durata? Come puoi immaginare, in questo mondo non puoi più usare la pila per ottimizzare la necessità di raccogliere attivazioni. L'insieme di attivazioni può contenere nuovamente buchi.

C # 2.0 ha questa funzionalità sotto forma di yield return. Un metodo che fa un rendimento ha intenzione di essere riattivato in un secondo momento - la prossima volta che viene chiamato MoveNext - e quando ciò accade non è prevedibile. Pertanto, le informazioni che sarebbero normalmente nello stack per il frame di attivazione del blocco iteratore vengono invece archiviate nell'heap, dove vengono raccolte garbage quando viene raccolto l'enumeratore.

Allo stesso modo, la funzione "asincrona / attende" disponibile nelle prossime versioni di C # e VB consentirà di creare metodi le cui attivazioni "producono" e "riprendono" in punti ben definiti durante l'azione del metodo. Poiché i frame di attivazione non vengono più creati e distrutti in modo prevedibile, tutte le informazioni che erano memorizzate nello stack dovranno essere archiviate nell'heap.

È solo un incidente della storia che ci è capitato di decidere per alcuni decenni che le lingue con frame di attivazione che sono stati creati e distrutti in un modo rigorosamente ordinato erano alla moda. Poiché alle lingue moderne manca sempre più questa proprietà, aspettatevi di vedere sempre più lingue che reiterano le continuazioni sul mucchio raccolto dall'immondizia, piuttosto che sulla pila.


13

La risposta più ovvia, e forse non la più completa, è che l'heap è la posizione dei dati dell'istanza. Per dati di istanza intendiamo i dati che rappresentano le istanze di classi, ovvero oggetti, che vengono creati in fase di esecuzione. Questi dati sono intrinsecamente dinamici e il numero di questi oggetti, e quindi la quantità di memoria che occupano, è noto solo in fase di esecuzione. Deve esserci qualche irritazione al recupero di questa memoria o programmi a lungo in esecuzione consumerebbero tutta la memoria presente nel tempo.

La memoria consumata da definizioni di classe, costanti e altre strutture di dati statici è intrinsecamente improbabile che aumenti senza controllo. Dato che esiste una sola definizione di classe in memoria per un numero sconosciuto di istanze di runtime di quella classe, ha senso che questo tipo di struttura non rappresenti una minaccia per l'utilizzo della memoria.


5
Ma l'heap non è la posizione dei "dati di istanza". Possono anche essere in pila.
svick

@svick Dipende dalla lingua, ovviamente. Java supporta solo oggetti allocati in heap e Vala distingue in modo abbastanza esplicito tra allocazione in heap (classe) e allocazione in stack (struct).
soffice

1
@fluffy: quelle sono lingue molto limitate, non si può presumere che ciò valga in generale poiché nessuna lingua è stata definita.
Matthieu M.,

@MatthieuM. Era una specie del mio punto.
soffice

@fluffy: allora perché le classi sono allocate nell'heap, mentre le strutture sono allocate nello stack?
Templare oscuro,

10

Vale la pena ricordare il motivo per cui abbiamo la garbage collection: perché a volte è difficile sapere quando deallocare la memoria. Hai davvero questo problema con l'heap. I dati allocati nello stack verranno infine deallocati, quindi non è davvero necessario eseguire la garbage collection lì. Si presume che le cose nella sezione dati siano allocate per la durata del programma.


1
Non solo sarà deallocato "alla fine", ma sarà deallocato al momento giusto.
Boris Yankov,

3
  1. La dimensione di questi è prevedibile (costante ad eccezione dello stack e lo stack è in genere limitato a pochi MB) e in genere molto piccolo (almeno rispetto alle centinaia di MB che le grandi applicazioni possono allocare).

  2. Gli oggetti allocati dinamicamente in genere hanno un piccolo lasso di tempo in cui sono raggiungibili. Dopodiché, non è più possibile fare riferimento a loro. Contrastalo con le voci nella sezione dati, le variabili globali e così via: frequentemente, c'è un pezzo di codice che le fa riferimento direttamente (pensa const char *foo() { return "foo"; }). Normalmente, il codice non cambia, quindi il riferimento è lì per rimanere e un altro riferimento verrà creato ogni volta che viene invocata la funzione (che potrebbe essere in qualsiasi momento per quanto ne sa il computer - a meno che non si risolva il problema di arresto, ovvero ). Pertanto, non è possibile liberare la maggior parte di quella memoria, poiché sarebbe sempre raggiungibile.

  3. In molti linguaggi raccolti dall'immondizia, tutto ciò che appartiene al programma in esecuzione è allocato in heap. In Python, semplicemente non c'è alcuna sezione di dati e nessun valore allocato in pila (ci sono i riferimenti che sono le variabili locali e c'è lo stack di chiamate, ma nessuno dei due ha un valore nello stesso senso di un intin C). Ogni oggetto è nell'heap.


"In Python, semplicemente non esiste alcuna sezione di dati". Questo non è strettamente vero. Nessuno, Vero e Falso sono allocati nella sezione dati come ho capito: stackoverflow.com/questions/7681786/how-is-hashnone-calculated
Jason Baker

@JasonBaker: scoperta interessante! Non ha alcun effetto però. È un dettaglio di implementazione e limitato agli oggetti incorporati. Che non è di dire che quegli oggetti non dovrebbero essere deallocato mai nella durata del programma in ogni caso, non sono, e sono anche molto piccolo in termini di dimensioni (meno di 32 byte ciascuno, direi).

@delnan Come sottolinea Eric Lippert, per la maggior parte delle lingue l'esistenza di aree di memoria separate per lo stack e l'heap è un dettaglio di implementazione. Puoi implementare la maggior parte delle lingue senza usare uno stack (anche se le prestazioni potrebbero risentirne) ed essere comunque conforme alle loro specifiche
Jules

2

Come diversi altri rispondenti hanno detto, lo stack fa parte del set di root, quindi viene scansionato per i riferimenti ma non "raccolto", di per sé.

Voglio solo rispondere ad alcuni dei commenti che implicano che l'immondizia nello stack non ha importanza; sì, perché potrebbe causare più spazzatura nell'heap da considerare raggiungibile. I creatori coscienti di VM e compilatori annullano o escludono in altro modo parti morte dello stack dalla scansione. IIRC, alcune VM hanno tabelle che mappano gli intervalli di PC in bitmap di stack-slot-liveness e altri semplicemente annullano gli slot. Non so quale tecnica sia attualmente preferita.

Un termine usato per descrivere questa particolare considerazione è sicuro per lo spazio .


Sarebbe interessante saperlo. Il primo pensiero è che l'annullamento degli spazi sia il più realistico. Attraversare un albero di aree escluse potrebbe richiedere più tempo della semplice scansione di valori null. Ovviamente ogni tentativo di compattare la pila è irto di pericoli! Fare in modo che il lavoro suoni come un processo che piega la mente / è soggetto a errori.
Brian Knoblauch,

@Brian, in realtà, pensandoci ancora un po ', per una VM tipizzata hai bisogno di qualcosa del genere comunque, quindi puoi determinare quali slot sono riferimenti invece di numeri interi, float, ecc. Inoltre, per quanto riguarda la compattazione dello stack, vedi "CONTRO Non contro i suoi argomenti "di Henry Baker.
Ryan Culpepper,

Determinare i tipi di slot e verificare che vengano utilizzati in modo appropriato può e di solito viene eseguito staticamente, sia in fase di compilazione (per VM che utilizzano un bytecode attendibile) o in fase di caricamento (in cui il bytecode proviene da una fonte non attendibile, ad esempio Java).
Jules,

1

Vorrei sottolineare alcune idee sbagliate fondamentali che tu e molti altri avete sbagliato:

"Perché Garbage Collection spazza solo l'heap?" È il contrario. Solo i bidoni della spazzatura più semplici, più conservatori e più lenti spazzano il mucchio. Ecco perché sono così lenti.

I garbage collector veloci spazzano solo lo stack (e facoltativamente alcune altre radici, come alcuni globi per i puntatori FFI e i registri per i puntatori attivi) e copiano solo i puntatori raggiungibili dagli oggetti dello stack. Il resto viene gettato via (cioè ignorato), non analizzando affatto l'heap.

Poiché l'heap è circa 1000 volte più grande degli stack, un GC con scansione dello stack in genere è molto più veloce. ~ 15ms contro 250ms su cumuli di dimensioni normali. Dal momento che sta copiando (spostando) gli oggetti da uno spazio all'altro, viene per lo più chiamato un raccoglitore di copie semi-spaziale, ha bisogno di 2x memoria e quindi per lo più non utilizzabile su dispositivi molto piccoli come telefoni con poca memoria. È compatto, quindi è molto compatibile con la cache in futuro, a differenza dei semplici scanner heap mark & ​​sweep.

Dal momento che si stanno muovendo puntatori, FFI, identità e riferimenti sono difficili. L'identità è di solito risolta con ID casuali, riferimenti tramite puntatori di inoltro. FFI è complicato, poiché gli oggetti estranei non possono trattenere i puntatori al vecchio spazio. I puntatori FFI vengono generalmente conservati in un'arena heap separata, ad esempio con un contrassegno lento e sweep, un collettore statico. O banale malloc con refcounting. Si noti che malloc ha un enorme sovraccarico e ricontatta ancora di più.

Mark & ​​Sweep è banale da implementare ma non dovrebbe essere utilizzato in programmi reali e soprattutto non deve essere insegnato come raccoglitore standard. Il più famoso di un tale raccoglitore di copie a scansione veloce è il raccoglitore a due dita Cheney .


La domanda sembra essere più su quali parti della memoria vengono raccolte in modo inutile, piuttosto che su specifici algoritmi di garbage collection. L'ultima frase implica in particolare che l'OP sta usando "sweep" come sinonimo generico di "garbage collection", piuttosto che un meccanismo specifico per implementare la garbage collection. Considerando ciò, la tua risposta sembra dire che solo i più semplici raccoglitori di immondizia raccolgono l'heap e i raccoglitori di rifiuti veloci invece di immondizia raccolgono lo stack e la memoria statica, lasciando che l'heap cresca e cresca fino a esaurire la memoria.
8

No, la domanda era molto specifica e intelligente. Le risposte non sono così. Contrassegno lento e sweep I GC hanno due fasi, la fase mark che scandisce le radici sullo stack e la fase sweep che scansiona l'heap. La copia veloce dei GC ha solo una fase, la scansione dello stack. Facile come quello. Dal momento che apparentemente nessuno sa qui dei veri e propri raccoglitori di immondizia, la domanda deve avere una risposta. La tua interpretazione è selvaggiamente off.
rurban,

0

Cosa viene allocato nello stack? Variabili locali e indirizzi di ritorno (in C). Quando una funzione ritorna, le sue variabili locali vengono scartate. Non è necessario, neppure dannoso, spazzare la pila.

Molti linguaggi dinamici, e anche Java o C # sono implementati in un linguaggio di programmazione del sistema, spesso in C. Si potrebbe dire che Java è implementato con funzioni C e usa variabili locali C e quindi il garbage collector di Java non ha bisogno di spazzare lo stack.

C'è un'eccezione interessante: il garbage collector di Chicken Scheme fa spazzare lo stack (in un certo senso), perché la sua implementazione utilizza lo stack come spazio di prima generazione della garbage collection: vedi Chicken Scheme Design Wikipedia .

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.