Perché gli oggetti Java non vengono eliminati immediatamente dopo che non sono più referenziati?


77

In Java, non appena un oggetto non ha più riferimenti, diventa idoneo per l'eliminazione, ma la JVM decide quando l'oggetto viene effettivamente eliminato. Per usare la terminologia Objective-C, tutti i riferimenti Java sono intrinsecamente "forti". Tuttavia, in Objective-C, se un oggetto non ha più riferimenti forti, l'oggetto viene eliminato immediatamente. Perché non è così in Java?


46
Non dovresti preoccuparti quando gli oggetti Java vengono effettivamente eliminati. È un dettaglio di implementazione.
Basile Starynkevitch

154
@BasileStarynkevitch Dovresti assolutamente preoccuparti e sfidare il funzionamento del tuo sistema / piattaforma. Porre domande su "come" e "perché" è uno dei modi migliori per diventare un miglior programmatore (e, in senso più generale, una persona più intelligente).
Artur Biesiadowski

6
Cosa fa l'Obiettivo C quando ci sono riferimenti circolari? Presumo che li perda?
Mehrdad,

45
@ArturBiesiadowksi: No, la specifica Java non dice quando viene eliminato un oggetto (e allo stesso modo, per R5RS ). Potresti e probabilmente dovresti sviluppare il tuo programma Java come se tale cancellazione non dovesse mai avvenire (e per processi di breve durata come un ciao mondo di Java, in effetti non succede). Potresti preoccuparti dell'insieme di oggetti viventi (o del consumo di memoria), che è una storia diversa.
Basile Starynkevitch

28
Un giorno il novizio disse al maestro "Ho una soluzione al nostro problema di allocazione. Daremo ad ogni allocazione un conteggio di riferimento e quando raggiunge lo zero, possiamo eliminare l'oggetto". Il maestro rispose "Un giorno il novizio disse al maestro" Ho una soluzione ...
Eric Lippert

Risposte:


79

Prima di tutto, Java ha riferimenti deboli e un'altra categoria di massimo sforzo chiamata riferimenti software. I riferimenti deboli e forti sono un problema completamente separato dal conteggio dei riferimenti rispetto alla raccolta dei rifiuti.

In secondo luogo, ci sono modelli nell'uso della memoria che possono rendere la raccolta dei rifiuti più efficiente nel tempo sacrificando lo spazio. Ad esempio, gli oggetti più recenti hanno molte più probabilità di essere eliminati rispetto agli oggetti più vecchi. Quindi, se aspetti un po 'tra gli sweep, puoi eliminare la maggior parte della nuova generazione di memoria, spostando i pochi sopravvissuti nella memoria a lungo termine. Quella memoria a lungo termine può essere scansionata molto meno frequentemente. La cancellazione immediata tramite la gestione manuale della memoria o il conteggio dei riferimenti è molto più soggetta alla frammentazione.

È un po 'come la differenza tra andare a fare la spesa una volta per busta paga e andare ogni giorno a prendere abbastanza cibo per un giorno. Il tuo unico grande viaggio richiederà molto più tempo di un singolo piccolo viaggio, ma nel complesso finisci per risparmiare tempo e probabilmente denaro.


58
La moglie di un programmatore lo manda al supermercato. Gli dice: "Compra una pagnotta di pane e se vedi delle uova, prendine una dozzina". Il programmatore in seguito ritorna con una dozzina di pagnotte sotto il braccio.
Neil

7
Suggerisco di menzionare che il tempo di gc di nuova generazione è generalmente proporzionale alla quantità di oggetti vivi , quindi avere più oggetti eliminati significa che il loro costo non verrà pagato affatto in molti casi. L'eliminazione è semplice come lanciare il puntatore dello spazio di sopravvivenza e azzerare facoltativamente l'intero spazio di memoria in un grande memset (non sono sicuro che sia fatto alla fine di gc o ammortizzato durante l'allocazione di tlabs o oggetti stessi negli attuali jvms)
Artur Biesiadowski

64
@Neil non dovrebbero essere 13 pagnotte?
JAD

67
"Off by one error on
aisle

13
@JAD avrei detto 13, ma la maggior parte non tende a capirlo. ;)
Neil

86

Perché conoscere correttamente qualcosa a cui non si fa più riferimento non è facile. Nemmeno vicino a facile.

Cosa succede se si hanno due oggetti che fanno riferimento l'uno all'altro? Rimangono per sempre? Estendendo questa linea di pensiero alla risoluzione di qualsiasi struttura di dati arbitraria, vedrai presto perché la JVM o altri garbage collector sono costretti a impiegare metodi molto più sofisticati per determinare ciò che è ancora necessario e cosa può andare.


7
Oppure potresti adottare un approccio Python in cui utilizzi il refounting il più possibile, ricorrendo a un GC quando ti aspetti che vi siano dipendenze circolari che perdono memoria. Non vedo perché non avrebbero potuto ricontattare oltre a GC?
Mehrdad,

27
@Mehrdad Potrebbero. Ma probabilmente sarebbe più lento. Nulla ti impedisce di implementare questo, ma non aspettarti di battere nessuno dei GC in Hotspot o OpenJ9.
Josef

21
@ jpmc26 perché se elimini oggetti non appena non vengono più utilizzati, la probabilità è alta li elimini in una situazione di carico elevato che aumenta ulteriormente il carico. GC può funzionare quando c'è meno carico. Il conteggio dei riferimenti stesso è un piccolo sovraccarico per ogni riferimento. Inoltre con un GC è spesso possibile scartare una grande porzione di memoria senza riferimenti senza gestire i singoli oggetti.
Josef

33
@Josef: il corretto conteggio dei riferimenti non è gratuito; l'aggiornamento del conteggio dei riferimenti richiede incrementi / decrementi atomici, che sono sorprendentemente costosi , specialmente sulle architetture multicore moderne. In CPython non è un grosso problema (CPython è estremamente lento da solo, e GIL limita le sue prestazioni multithread a livelli single-core), ma su un linguaggio più veloce che supporta anche il parallelismo può essere un problema. Non è una possibilità che PyPy elimini completamente il conteggio dei riferimenti e usi semplicemente GC.
Matteo Italia,

10
@Mehrdad, una volta implementato il tuo conteggio dei riferimenti GC per Java, lo testerò volentieri per trovare un caso in cui funziona peggio di qualsiasi altra implementazione GC.
Josef

45

AFAIK, la specifica JVM (scritta in inglese) non menziona quando esattamente un oggetto (o un valore) dovrebbe essere cancellato e lo lascia all'implementazione (anche per R5RS ). In qualche modo richiede o suggerisce un garbage collector ma lascia i dettagli all'implementazione. E allo stesso modo per le specifiche Java.

Ricorda che i linguaggi di programmazione sono specifiche (di sintassi , semantica , ecc ...), non implementazioni di software. Un linguaggio come Java (o la sua JVM) ha molte implementazioni. Le sue specifiche sono pubblicate , scaricabili (in modo da poterle studiare) e scritte in inglese. §2.5.3 L'heap delle specifiche JVM menziona un garbage collector:

L'archiviazione dell'heap per gli oggetti viene recuperata da un sistema di gestione dell'archiviazione automatica (noto come garbage collector); gli oggetti non vengono mai deallocati esplicitamente. La Java Virtual Machine non assume alcun tipo particolare di sistema di gestione dello storage automatico

(l'enfasi è mia; la finalizzazione BTW è menzionata nel §12.6 delle specifiche Java e un modello di memoria è nel §17.4 delle specifiche Java)

Quindi (in Java) non dovresti preoccuparti quando un oggetto viene eliminato e potresti codificare come se non dovesse accadere (ragionando in un'astrazione in cui lo ignori). Ovviamente devi preoccuparti del consumo di memoria e del set di oggetti viventi, che è una domanda diversa . In molti semplici casi (pensate a un programma "ciao mondo") siete in grado di dimostrare, o per convincervi, che la memoria allocata è piuttosto piccola (ad esempio meno di un gigabyte), e quindi non vi interessa affatto cancellazione di singoli oggetti. In più casi, puoi convincerti che gli oggetti viventi(o quelli raggiungibili, che è un superset - più facile da ragionare - di quelli viventi) non superano mai un limite ragionevole (e quindi fai affidamento su GC, ma non ti importa come e quando avviene la raccolta dei rifiuti). Leggi la complessità dello spazio .

Immagino che su diverse implementazioni JVM che eseguono un programma Java di breve durata come un ciao mondo, il garbage collector non sia affatto attivato e non si verifichi alcuna eliminazione. AFAIU, un simile comportamento è conforme alle numerose specifiche Java.

La maggior parte delle implementazioni JVM utilizzano tecniche di copia generazionale (almeno per la maggior parte degli oggetti Java, quelli che non usano la finalizzazione o riferimenti deboli ; la finalizzazione non è garantita in breve tempo e potrebbe essere posticipata, quindi è solo una funzione utile che il codice non dovrebbe dipende molto da) in cui la nozione di cancellare un singolo oggetto non ha alcun senso (dal momento che un grande blocco di memoria contenente zone di memoria per molti oggetti, forse diversi megabyte contemporaneamente, viene rilasciato contemporaneamente).

Se la specifica JVM richiedesse che ogni oggetto venisse cancellato il più presto possibile (o semplicemente ponesse ulteriori vincoli alla cancellazione dell'oggetto), sarebbero vietate tecniche GC generazionali efficienti e i progettisti di Java e della JVM sarebbero stati saggi nel evitarlo.

A proposito, potrebbe essere possibile che una JVM ingenua che non cancella mai oggetti e non rilasci memoria possa essere conforme alle specifiche (la lettera, non lo spirito) e certamente è in grado di eseguire un ciao mondo in pratica (nota che la maggior parte i programmi Java piccoli e di breve durata probabilmente non allocano più di qualche gigabyte di memoria). Naturalmente una JVM del genere non merita di essere menzionata ed è solo una cosa giocattolo (come questa implementazione di mallocper C). Per ulteriori informazioni, consultare Epsilon NoOp GC . Le JVM reali sono software molto complessi e mescolano diverse tecniche di raccolta dei rifiuti.

Inoltre, Java non è lo stesso di JVM e le implementazioni Java sono in esecuzione senza JVM (ad es. Compilatori Java anticipati , runtime Android ). In alcuni casi (principalmente accademici), potresti immaginare (le cosiddette tecniche di "raccolta dei dati inutili") che un programma Java non alloca o elimina in fase di esecuzione (ad esempio perché il compilatore di ottimizzazione è stato abbastanza intelligente da utilizzare solo il stack di chiamate e variabili automatiche ).

Perché gli oggetti Java non vengono eliminati immediatamente dopo che non sono più referenziati?

Perché le specifiche Java e JVM non lo richiedono.


Leggi il manuale GC per ulteriori informazioni (e le specifiche JVM ). Si noti che essere vivo (o utile per il calcolo futuro) per un oggetto è una proprietà dell'intero programma (non modulare).

Objective-C favorisce un approccio di conteggio dei riferimenti alla gestione della memoria . E che ha anche insidie (ad esempio, l'Objective-C programmatore deve preoccuparsi di riferimenti circolari da esplicitando riferimenti deboli, ma una JVM gestisce riferimenti circolari bene in pratica senza richiedere l'attenzione da parte del programmatore Java).

Non esiste Silver Bullet nella programmazione e nella programmazione del linguaggio di programmazione (sii consapevole del problema Halting ; essere un oggetto vivente utile è ineccepibile in generale).

Puoi anche leggere SICP , Pragmatica del linguaggio di programmazione , Dragon Book , Lisp in piccoli pezzi e sistemi operativi: tre pezzi facili . Non si tratta di Java, ma apriranno la tua mente e dovrebbero aiutare a capire cosa dovrebbe fare una JVM e come potrebbe praticamente funzionare (con altri pezzi) sul tuo computer. Potresti anche dedicare molti mesi (o diversi anni) allo studio del complesso codice sorgente delle implementazioni JVM open source esistenti (come OpenJDK , che ha diversi milioni di righe di codice sorgente).


20
"potrebbe essere possibile che una JVM ingenua che non cancella mai oggetti e non rilasci memoria possa essere conforme alle specifiche" Sicuramente è conforme alle specifiche! Java 11 in realtà sta aggiungendo un garbage collector no-op per, tra le altre cose, programmi di breve durata.
Michael,

6
"non dovresti preoccuparti quando un oggetto viene eliminato" Non sono d'accordo. Per uno, dovresti sapere che RAII non è più un modello fattibile e che non puoi dipendere finalizeda alcuna gestione delle risorse (di filehandle, connessioni db, risorse gpu, ecc.).
Alexander

4
@Michael Ha perfettamente senso per l'elaborazione batch con un limite di memoria usato. Il sistema operativo può semplicemente dire "ora tutta la memoria utilizzata da questo programma è sparita!" dopo tutto, che è piuttosto veloce. In effetti, molti programmi in C sono stati scritti in quel modo, specialmente nel primo mondo Unix. Pascal ha avuto l'incredibilmente orribile "reimpostare il puntatore stack / heap su un checkpoint pre-salvato" che ti permetteva di fare praticamente la stessa cosa, anche se era abbastanza pericoloso: contrassegnare, avviare un sottoattività, ripristinare.
Luaan,

6
@Alexander in generale al di fuori del C ++ (e alcuni linguaggi che ne derivano intenzionalmente), supponendo che RAII funzionerà basandosi solo sui finalizzatori è un anti-pattern, che dovrebbe essere messo in guardia e sostituito con un blocco esplicito di controllo delle risorse. Il punto centrale di GC è che la vita e le risorse sono disaccoppiate, dopo tutto.
Leushenko,

3
@Leushenko Non sono assolutamente d'accordo sul fatto che "la vita e le risorse sono disaccoppiate" è il "punto" di GC. È il prezzo negativo che si paga per il punto principale di GC: gestione della memoria semplice e sicura. "supponendo che RAII funzionerà basandosi solo sui finalizzatori è un anti-pattern" In Java? Forse. Ma non in CPython, Rust, Swift o Objective C. "messi in guardia e sostituiti con un blocco esplicito di controllo delle risorse" No, questi sono strettamente più limitati. Un oggetto che gestisce una risorsa tramite RAII ti dà una maniglia per passare la vita con ambito. Un blocco try-with-resource è limitato a un singolo ambito.
Alexander

23

Per usare la terminologia Objective-C, tutti i riferimenti Java sono intrinsecamente "forti".

Non è corretto: Java ha riferimenti sia deboli che deboli, sebbene siano implementati a livello di oggetto anziché come parole chiave del linguaggio.

In Objective-C, se un oggetto non ha più riferimenti forti, l'oggetto viene eliminato immediatamente.

Anche questo non è necessariamente corretto: alcune versioni di Objective C hanno effettivamente utilizzato un garbage collector generazionale. Altre versioni non avevano alcuna raccolta dei rifiuti.

È vero che le versioni più recenti di Objective C usano il conteggio dei riferimenti automatico (ARC) anziché un GC basato sulla traccia, e ciò (spesso) fa sì che l'oggetto venga "cancellato" quando quel conteggio dei riferimenti raggiunge lo zero. Tuttavia, si noti che un'implementazione JVM potrebbe anche essere conforme e funzionare esattamente in questo modo (diamine, potrebbe essere conforme e non avere alcun GC).

Quindi perché la maggior parte delle implementazioni JVM non lo fa e utilizza invece algoritmi GC basati su traccia?

In poche parole, ARC non è così utopistico come sembra inizialmente:

  • Devi incrementare o decrementare un contatore ogni volta che un riferimento viene copiato, modificato o esce dal campo di applicazione, il che comporta un evidente sovraccarico di prestazioni.
  • ARC non può cancellare facilmente i riferimenti ciclici, poiché tutti hanno un riferimento reciproco, quindi il loro conteggio dei riferimenti non raggiunge mai zero.

ARC ha ovviamente dei vantaggi: è semplice da implementare e la raccolta è deterministica. Ma gli svantaggi di cui sopra, tra gli altri, sono il motivo per cui la maggior parte delle implementazioni di JVM utilizzerà un GC generazionale basato sulla traccia.


1
La cosa divertente è che Apple è passata ad ARC proprio perché ha visto che, in pratica, supera ampiamente gli altri GC (in particolare quelli generazionali). Ad essere onesti, questo è principalmente vero su piattaforme con memoria limitata (iPhone). Ma contrasterò la tua affermazione che "ARC non è così utopico come sembra inizialmente" dicendo che i GC generazionali (e altri non deterministici) non sono così utopistici come sembrano inizialmente: la distruzione deterministica è probabilmente un'opzione migliore nel vasta maggioranza di scenari.
Konrad Rudolph,

3
@KonradRudolph anche se sono anche un fan della distruzione deterministica, non credo che "l'opzione migliore nella stragrande maggioranza degli scenari" regge. È certamente un'opzione migliore quando la latenza o la memoria sono più importanti della velocità media, e in particolare quando la logica è ragionevolmente semplice. Ma non è che non ci siano molte applicazioni complesse che richiedono molti riferimenti ciclici ecc. E richiedono un funzionamento medio veloce, ma non si preoccupano davvero della latenza e hanno molta memoria disponibile. Per questi, è dubbio che ARC sia una buona idea.
lasciato circa

1
@leftaroundabout Nella "maggior parte degli scenari", né la velocità effettiva né la pressione della memoria sono un collo di bottiglia, quindi non importa in alcun modo. Il tuo esempio è uno scenario specifico. Certo, non è estremamente raro ma non andrei fino al punto di affermare che è più comune di altri scenari in cui ARC è più adatto. Inoltre, ARC può gestire bene i cicli. Richiede solo un semplice intervento manuale da parte del programmatore. Questo lo rende meno ideale, ma difficilmente un rompicapo. Io sostengo che la finalizzazione deterministica sia una caratteristica molto più importante di quanto tu finga.
Konrad Rudolph,

3
@KonradRudolph Se ARC richiede un semplice intervento manuale da parte del programmatore, allora non si occupa dei cicli. Se inizi a utilizzare pesantemente elenchi doppiamente collegati, ARC passa all'allocazione manuale della memoria. Se hai grafici arbitrari di grandi dimensioni, ARC ti costringe a scrivere un garbage collector. L'argomento GC sarebbe che le risorse che necessitano di distruzione non sono il lavoro del sottosistema di memoria e, per tracciarne relativamente poche, dovrebbero essere finalizzate deterministicamente attraverso un semplice intervento manuale da parte del programmatore.
prosfilaes,

2
@KonradRudolph ARC e cicli porta fondamentalmente a perdite di memoria se non vengono gestite manualmente. In sistemi abbastanza complessi, possono verificarsi perdite importanti se ad esempio un oggetto memorizzato in una mappa memorizza un riferimento a quella mappa, una modifica che potrebbe essere fatta da un programmatore non responsabile delle sezioni di codice che creano e distruggono quella mappa. I grafici arbitrari di grandi dimensioni non significano che i puntatori interni non sono forti, che va bene che gli elementi collegati scompaiano. Se affrontare alcune perdite di memoria è meno un problema che dover chiudere manualmente i file, non lo dirò, ma è reale.
prosfilaes,

5

Java non specifica con precisione quando l'oggetto viene raccolto perché ciò dà alle implementazioni la libertà di scegliere come gestire la garbage collection.

Esistono molti diversi meccanismi di garbage collection, ma quelli che garantiscono che è possibile raccogliere immediatamente un oggetto si basano quasi interamente sul conteggio dei riferimenti (non sono a conoscenza di alcun algoritmo che interrompa questa tendenza). Il conteggio dei riferimenti è uno strumento potente, ma ha un costo per mantenere il conteggio dei riferimenti. Nel codice con filetto singolo, questo non è altro che un incremento e un decremento, quindi l'assegnazione di un puntatore può costare un costo nell'ordine di 3 volte tanto nel codice contato di riferimento rispetto a quello nel codice contato non di riferimento (se il compilatore può eseguire tutto il processo sulla macchina codice).

Nel codice multithread, il costo è più elevato. Richiede incrementi / decrementi atomici o blocchi, entrambi i quali possono essere costosi. Su un processore moderno, un'operazione atomica può essere dell'ordine di 20 volte più costosa di una semplice operazione di registro (ovviamente varia da processore a processore). Questo può aumentare il costo.

Quindi, con questo, possiamo considerare i compromessi fatti da diversi modelli.

  • Objective-C si concentra su ARC - conteggio dei riferimenti automatizzato. Il loro approccio consiste nell'utilizzare il conteggio dei riferimenti per tutto. Non esiste un rilevamento dei cicli (di cui sono a conoscenza), quindi i programmatori dovrebbero impedire che si verifichino cicli, il che comporta costi di sviluppo. La loro teoria è che i puntatori non vengono assegnati così spesso e il loro compilatore può identificare situazioni in cui l'incremento / decremento dei conteggi di riferimento non può causare la morte di un oggetto ed eludere completamente tali incrementi / decrementi. Pertanto minimizzano il costo del conteggio dei riferimenti.

  • CPython utilizza un meccanismo ibrido. Usano i conteggi dei riferimenti, ma hanno anche un garbage collector che identifica i cicli e li rilascia. Ciò offre i vantaggi di entrambi i mondi, al costo di entrambi gli approcci. CPython deve mantenere entrambi i conteggi dei riferimenti etenere il libro per rilevare i cicli. CPython se la cava in due modi. Il pugno è che CPython non è realmente completamente multithread. Ha un lucchetto noto come GIL che limita il multithreading. Questo significa che CPython può usare incrementi / decrementi normali piuttosto che atomici, che è molto più veloce. Viene anche interpretato CPython, il che significa che operazioni come l'assegnazione a una variabile richiedono già una manciata di istruzioni anziché solo 1. Il costo aggiuntivo per eseguire gli incrementi / decrementi, che viene eseguito rapidamente nel codice C, è meno problematico perché noi " ho già pagato questo costo.

  • Java segue l'approccio di non garantire affatto un sistema contato di riferimento. In effetti la specifica non dice nulla su come gli oggetti sono gestiti se non che ci sarà un sistema di gestione della memorizzazione automatica. Tuttavia, la specifica suggerisce anche fortemente che si tratterà di immondizia raccolta in modo da gestire i cicli. Non specificando quando gli oggetti scadono, java ottiene la libertà di usare collezionisti che non perdono tempo a incrementare / decrementare. In effetti, algoritmi intelligenti come i generatori di rifiuti generazionali possono persino gestire molti casi semplici senza nemmeno guardare i dati che vengono recuperati (devono solo guardare i dati a cui si fa ancora riferimento).

Quindi possiamo vedere ognuno di questi tre ha dovuto fare dei compromessi. Qual è il miglior compromesso dipende in gran parte dalla natura del modo in cui la lingua deve essere utilizzata.


4

Sebbene sia finalizestato appoggiato sul GC di Java, la raccolta dei rifiuti al suo interno non è interessata agli oggetti morti, ma a quelli vivi. Su alcuni sistemi GC (possibilmente includendo alcune implementazioni di Java), l'unica cosa che distingue un gruppo di bit che rappresenta un oggetto da un gruppo di archiviazione che non viene utilizzato per nulla potrebbe essere l'esistenza di riferimenti al primo. Mentre gli oggetti con i finalizzatori vengono aggiunti a un elenco speciale, altri oggetti potrebbero non avere nulla in qualsiasi parte dell'universo che indichi che il loro archivio è associato a un oggetto ad eccezione dei riferimenti contenuti nel codice utente. Quando l'ultimo di tali riferimenti viene sovrascritto, il modello di bit in memoria cesserà immediatamente di essere riconoscibile come oggetto, indipendentemente dal fatto che qualcosa nell'universo ne sia consapevole.

Lo scopo della garbage collection non è quello di distruggere oggetti per i quali non esistono riferimenti, ma piuttosto di realizzare tre cose:

  1. Riferimenti deboli non validi che identificano oggetti a cui non sono associati riferimenti fortemente raggiungibili.

  2. Cerca nell'elenco di oggetti del sistema con i finalizzatori per vedere se qualcuno di questi non ha riferimenti fortemente raggiungibili associati.

  3. Identificare e consolidare le aree di archiviazione che non vengono utilizzate da alcun oggetto.

Si noti che l'obiettivo principale del GC è il n. 3 e più si attende prima di farlo, maggiori sono le possibilità di consolidamento che si avranno. Ha senso fare # 3 nei casi in cui si avrebbe un uso immediato per l'archiviazione, ma altrimenti ha più senso rimandarlo.


5
In realtà, gc ha un solo obiettivo: simulare la memoria infinita. Tutto ciò che hai nominato come obiettivo è un'imperfezione nell'astrazione o un dettaglio di implementazione.
Deduplicatore

@Deduplicator: i riferimenti deboli offrono una semantica utile che non può essere raggiunta senza l'assistenza del GC.
supercat

Certo, i riferimenti deboli hanno una semantica utile. Ma quella semantica sarebbe necessaria se la simulazione fosse migliore?
Deduplicatore

@Deduplicator: Sì. Si consideri una raccolta che definisce come gli aggiornamenti interagiranno con l'enumerazione. Potrebbe essere necessario che tale raccolta contenga riferimenti deboli a qualsiasi enumeratore attivo. In un sistema a memoria illimitata, una raccolta ripetuta ripetutamente farebbe crescere senza limiti il ​​suo elenco di enumeratori interessati. La memoria richiesta per quell'elenco non sarebbe un problema, ma il tempo necessario per scorrere attraverso di esso degraderebbe le prestazioni del sistema. L'aggiunta di GC può significare la differenza tra un algoritmo O (N) e O (N ^ 2).
supercat

2
Perché dovresti voler avvisare gli enumeratori, invece di aggiungere un elenco e far loro cercare se stessi quando vengono utilizzati? E ogni programma che dipende dall'immondizia viene elaborato in modo tempestivo invece che in base alla pressione della memoria vive comunque in uno stato di peccato, se si muove affatto.
Deduplicatore

4

Vorrei suggerire una riformulazione e una generalizzazione della tua domanda:

Perché Java non offre forti garanzie sul processo GC?

Con questo in mente, fai scorrere rapidamente le risposte qui. Ci sono sette finora (senza contare questo), con alcuni thread di commenti.

Questa è la tua risposta

GC è difficile. Ci sono molte considerazioni, molti compromessi diversi e, alla fine, molti approcci molto diversi. Alcuni di questi approcci rendono possibile GC un oggetto non appena non è necessario; altri no. Mantenendo il contratto libero, Java offre ai suoi implementatori più opzioni.

Ovviamente c'è un compromesso anche in quella decisione: mantenendo libero il contratto, Java per lo più * toglie ai programmatori la possibilità di fare affidamento sui distruttori. Questo è qualcosa che i programmatori C ++ in particolare spesso mancano ([citazione necessaria];)), quindi non è un compromesso insignificante. Non ho visto una discussione su quella particolare meta-decisione, ma presumibilmente la gente di Java ha deciso che i vantaggi di avere più opzioni GC erano superiori ai vantaggi di essere in grado di dire ai programmatori esattamente quando un oggetto verrà distrutto.


* Esiste il finalizemetodo, ma per vari motivi che non rientrano nell'ambito di questa risposta, è difficile e non è una buona idea fare affidamento su di esso.


3

Esistono due diverse strategie di gestione della memoria senza codice esplicito scritto dallo sviluppatore: Garbage Collection e conteggio dei riferimenti.

Garbage Collection ha il vantaggio di "funzionare" a meno che lo sviluppatore non faccia qualcosa di stupido. Con il conteggio dei riferimenti, puoi avere cicli di riferimento, il che significa che "funziona" ma lo sviluppatore a volte deve essere intelligente. Quindi questo è un vantaggio per la raccolta dei rifiuti.

Con il conteggio dei riferimenti, l'oggetto scompare immediatamente quando il conteggio dei riferimenti scende a zero. Questo è un vantaggio per il conteggio dei riferimenti.

Rapidamente, la garbage collection è più veloce se credi ai fan della garbage collection e il conteggio dei riferimenti è più veloce se credi ai fan del conteggio dei riferimenti.

Sono solo due metodi diversi per raggiungere lo stesso obiettivo, Java ha scelto un metodo, Objective-C ne ha scelto un altro (e ha aggiunto un sacco di supporto al compilatore per cambiarlo da qualcosa di doloroso in qualcosa che è poco lavoro per gli sviluppatori).

Cambiare Java dalla garbage collection al conteggio dei riferimenti sarebbe una grande impresa, poiché sarebbero necessarie molte modifiche al codice.

In teoria, Java avrebbe potuto implementare una combinazione di garbage collection e conteggio dei riferimenti: se il conteggio dei riferimenti è 0, l'oggetto è irraggiungibile, ma non necessariamente viceversa. Quindi è possibile mantenere i conteggi dei riferimenti ed eliminare gli oggetti quando il loro conteggio dei riferimenti è zero (e quindi eseguire la garbage collection di volta in volta per catturare gli oggetti all'interno di cicli di riferimento non raggiungibili). Penso che il mondo sia diviso 50/50 nelle persone che pensano che aggiungere il conteggio dei riferimenti alla raccolta dei rifiuti sia una cattiva idea, e le persone che pensano che aggiungere la raccolta dei rifiuti al conteggio dei riferimenti sia una cattiva idea. Quindi questo non accadrà.

Quindi Java potrebbe eliminare immediatamente gli oggetti se il loro conteggio di riferimento diventa zero ed eliminare gli oggetti entro cicli non raggiungibili in seguito. Ma questa è una decisione di progettazione e Java ha deciso di non farlo.


Con il conteggio dei riferimenti, la finalizzazione è banale, poiché il programmatore si è occupato dei cicli. Con gc, i cicli sono banali, ma il programmatore deve fare attenzione con la finalizzazione.
Deduplicatore

@Deduplicator In Java, è anche possibile creare forti riferimenti a oggetti che sono finalizzati ... In Objective-C e Swift, una volta che il conteggio di riferimento è pari a zero, l'oggetto verrà scompare (a meno che non si mette un ciclo infinito in dealloc / deist).
gnasher729,

Ho appena notato uno stupido correttore ortografico che sostituisce deinit con deist ...
gnasher729

1
C'è un motivo per cui molti programmatori odiano la correzione automatica dell'ortografia ... ;-)
Deduplicator

lol ... Penso che il mondo sia diviso 0.1 / 0.1 / 99.8 tra le persone che pensano che aggiungere il conteggio dei riferimenti alla raccolta dei rifiuti sia una cattiva idea, e le persone che pensano che aggiungere la raccolta dei rifiuti al conteggio dei riferimenti sia una cattiva idea, e le persone che continua a contare i giorni fino all'arrivo della raccolta dei rifiuti perché quella tonnellata sta già diventando di nuovo puzzolente ...
leftaroundabout

1

Tutti gli altri argomenti e discussioni sulla performance relativi alla difficoltà di comprensione quando non ci sono più riferimenti a un oggetto sono corretti sebbene un'altra idea che ritengo degna di menzione sia che esista almeno una JVM (azul) che considera qualcosa del genere in quanto implementa parallel gc che ha essenzialmente un thread vm che controlla costantemente i riferimenti per tentare di eliminarli che agiranno in modo non del tutto diverso da quello di cui stai parlando. Fondamentalmente si guarderà costantemente attorno all'heap e proverà a recuperare qualsiasi memoria a cui non si fa riferimento. Ciò comporta un costo prestazionale molto leggero ma porta a tempi GC sostanzialmente nulli o molto brevi. (Questo a meno che le dimensioni dell'heap in costante espansione non superino la RAM di sistema e quindi Azul si confonda e poi ci siano i draghi)

TLDR Qualcosa del genere esiste per la JVM, è solo una jvm speciale e presenta degli svantaggi come qualsiasi altro compromesso ingegneristico.

Disclaimer: non ho legami con Azul, l'abbiamo appena usato in un precedente lavoro.


1

Massimizzare la produttività sostenuta o minimizzare la latenza gc sono in tensione dinamica, che è probabilmente il motivo più comune per cui GC non si verifica immediatamente. In alcuni sistemi, come le app di emergenza 911, il mancato rispetto di una soglia di latenza specifica può avviare i processi di failover del sito. In altri, come un sito bancario e / o di arbitraggio, è molto più importante massimizzare il rendimento.


0

Velocità

Perché tutto ciò sta accadendo è in definitiva a causa della velocità. Se i processori erano infinitamente veloci o (per essere pratici) vicini, ad esempio 1.000.000.000.000.000.000.000.000.000.000.000 di operazioni al secondo, allora potresti avere cose insanamente lunghe e complicate tra ogni operatore, come assicurarti che gli oggetti de-referenziati vengano eliminati. Dato che quel numero di operazioni al secondo non è attualmente vero e, come la maggior parte delle altre risposte spiega che in realtà è complicato e dispendioso in termini di risorse per capirlo, esiste la garbage collection in modo che i programmi possano concentrarsi su ciò che stanno effettivamente cercando di ottenere in un modo rapido.


Bene, sono sicuro che troveremmo modi più interessanti di utilizzare i cicli extra di quello.
Deduplicatore,
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.