L'utilizzo di final per le variabili in Java migliora la garbage collection?


86

Oggi io e i miei colleghi abbiamo una discussione sull'utilizzo della finalparola chiave in Java per migliorare la garbage collection.

Ad esempio, se scrivi un metodo come:

public Double doCalc(final Double value)
{
   final Double maxWeight = 1000.0;
   final Double totalWeight = maxWeight * value;
   return totalWeight;  
}

Dichiarare le variabili nel metodo finalaiuterebbe la garbage collection a ripulire la memoria dalle variabili inutilizzate nel metodo dopo che il metodo è uscito.

È vero?


ci sono due cose qui, in realtà. 1) quando scrivi in ​​un campo locale del metodo e in un'istanza. Quando scrivi a un'istanza potrebbe esserci un vantaggio.
Eugene

Risposte:


86

Ecco un esempio leggermente diverso, uno con i campi del tipo di riferimento finale anziché le variabili locali del tipo di valore finale:

public class MyClass {

   public final MyOtherObject obj;

}

Ogni volta che crei un'istanza di MyClass, creerai un riferimento in uscita a un'istanza di MyOtherObject e il GC dovrà seguire quel collegamento per cercare oggetti attivi.

La JVM utilizza un algoritmo GC mark-sweep, che deve esaminare tutti i riferimenti attivi nelle posizioni "radice" del GC (come tutti gli oggetti nello stack di chiamate corrente). Ogni oggetto attivo viene "contrassegnato" come vivo e anche qualsiasi oggetto a cui fa riferimento un oggetto attivo viene contrassegnato come vivo.

Dopo il completamento della fase di contrassegno, il GC esplora l'heap, liberando memoria per tutti gli oggetti non contrassegnati (e compattando la memoria per gli oggetti attivi rimanenti).

Inoltre, è importante riconoscere che la memoria heap Java è suddivisa in una "generazione giovane" e una "vecchia generazione". Tutti gli oggetti sono inizialmente assegnati alla giovane generazione (a volte indicata come "la scuola materna"). Poiché la maggior parte degli oggetti ha vita breve, il GC è più aggressivo nel liberare i rifiuti recenti dalle giovani generazioni. Se un oggetto sopravvive a un ciclo di raccolta della giovane generazione, viene spostato nella vecchia generazione (a volte indicata come "generazione di ruolo"), che viene elaborata meno frequentemente.

Quindi, dalla parte superiore della mia testa, dirò "no, il modificatore 'finale' non aiuta il GC a ridurre il suo carico di lavoro".

A mio parere, la migliore strategia per ottimizzare la gestione della memoria in Java è eliminare i riferimenti spuri il più rapidamente possibile. Puoi farlo assegnando "null" a un riferimento a un oggetto non appena hai finito di usarlo.

O, meglio ancora, ridurre al minimo la dimensione di ogni ambito di dichiarazione. Ad esempio, se dichiari un oggetto all'inizio di un metodo di 1000 righe e se l'oggetto rimane attivo fino alla chiusura dell'ambito di quel metodo (l'ultima parentesi graffa di chiusura), l'oggetto potrebbe rimanere in vita per molto più tempo di quanto effettivamente necessario.

Se si utilizzano metodi piccoli, con solo una dozzina circa di righe di codice, gli oggetti dichiarati all'interno di quel metodo usciranno più rapidamente dall'ambito e il GC sarà in grado di svolgere la maggior parte del suo lavoro all'interno del molto più efficiente giovane generazione. Non vuoi che gli oggetti vengano spostati nella vecchia generazione a meno che non sia assolutamente necessario.


Cibo per la mente. Ho sempre pensato che il codice inline fosse più veloce, ma se jvm ha poca memoria, rallenterà anche lui. hmmmmm ...
WolfmanDragon

1
Sto solo indovinando qui ... ma presumo che il compilatore JIT possa incorporare valori primitivi finali (non oggetti), per aumenti modesti delle prestazioni. Il codice inlining, d'altra parte, è in grado di produrre ottimizzazioni significative, ma non ha nulla a che fare con le variabili finali.
Benjismith

2
Non sarebbe possibile assegnare null a un oggetto finale già creato, quindi forse definitivo invece di aiuto, potrebbe rendere le cose più difficili
Hernán Eche

1
Si potrebbe usare {} anche per limitare l'ambito in un metodo di grandi dimensioni, piuttosto che suddividerlo in diversi metodi privati ​​che potrebbero non essere rilevanti per altri metodi di classe.
mmm

È inoltre possibile utilizzare variabili locali al posto dei campi quando è possibile per migliorare la garbage collection della memoria e ridurre i legami referenziali.
sivi

37

La dichiarazione di una variabile locale finalnon influirà sulla garbage collection, significa solo che non è possibile modificare la variabile. Il tuo esempio sopra non dovrebbe essere compilato poiché stai modificando la variabile totalWeightche è stata contrassegnata final. D'altra parte, la dichiarazione di una primitiva ( doubleinvece di Double) finalconsentirà a quella variabile di essere inline nel codice chiamante, in modo che possa causare un miglioramento della memoria e delle prestazioni. Questo è usato quando hai un numero di public static final Stringsin una classe.

In generale, il compilatore e il runtime ottimizzeranno dove possibile. È meglio scrivere il codice in modo appropriato e non cercare di essere troppo complicato. Utilizzare finalquando non si desidera che la variabile venga modificata. Supponiamo che qualsiasi facile ottimizzazione venga eseguita dal compilatore e, se sei preoccupato per le prestazioni o l'utilizzo della memoria, usa un profiler per determinare il vero problema.


26

No, enfaticamente non è vero.

Ricorda che finalnon significa costante, significa solo che non puoi cambiare il riferimento.

final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.

Potrebbe esserci qualche piccola ottimizzazione basata sulla consapevolezza che la JVM non dovrà mai modificare il riferimento (come non dover controllare per vedere se è cambiato) ma sarebbe così minore da non preoccuparsi.

Final dovrebbe essere pensato come metadati utili per lo sviluppatore e non come un'ottimizzazione del compilatore.


17

Alcuni punti da chiarire:

  • L'annullamento del riferimento non dovrebbe aiutare GC. Se così fosse, indicherebbe che le tue variabili sono troppo limitate. Un'eccezione è il caso del nepotismo degli oggetti.

  • Non esiste ancora un'allocazione sullo stack in Java.

  • Dichiarare una variabile finale significa che non puoi (in condizioni normali) assegnare un nuovo valore a quella variabile. Poiché il finale non dice nulla sull'ambito, non dice nulla sui suoi effetti su GC.


C'è un'allocazione sullo stack (di primitive e riferimenti a oggetti nell'heap) in java: stackoverflow.com/a/8061692/32453 Java richiede che anche gli oggetti nella stessa chiusura di una classe anonima / lambda siano finali, ma si trasforma fuori che è solo per ridurre la "memoria richiesta / frame" / ridurre la confusione quindi non è correlato alla sua raccolta ...
rogerdpack

11

Ebbene, non conosco l'uso del modificatore "finale" in questo caso, o il suo effetto sul GC.

Ma posso dirti questo: il tuo uso di valori Boxed piuttosto che di primitive (ad esempio, Double invece di double) allocherà quegli oggetti sull'heap piuttosto che sullo stack e produrrà spazzatura non necessaria che il GC dovrà ripulire.

Uso le primitive in box solo quando richiesto da un'API esistente o quando ho bisogno di primitive nullable.


1
Hai ragione. Avevo solo bisogno di un rapido esempio per spiegare la mia domanda.
Goran Martinic

5

Le variabili finali non possono essere modificate dopo l'assegnazione iniziale (applicata dal compilatore).

Ciò non modifica il comportamento della garbage collection in quanto tale. L'unica cosa è che queste variabili non possono essere annullate quando non vengono più utilizzate (il che può aiutare la raccolta dei rifiuti in situazioni di memoria ridotta).

Dovresti sapere che final consente al compilatore di fare ipotesi su cosa ottimizzare. Codice inline e codice escluso noto per non essere raggiungibile.

final boolean debug = false;

......

if (debug) {
  System.out.println("DEBUG INFO!");
}

Il println non sarà incluso nel codice byte.


@Eugene Dipende dal tuo responsabile della sicurezza e se il compilatore ha inserito o meno la variabile.
Thorbjørn Ravn Andersen

giusto, stavo solo facendo il pedante; niente di più; ha anche fornito una risposta
Eugene

4

C'è un caso d'angolo non così noto con i netturbini generazionali. (Per una breve descrizione leggi la risposta di benjismith per una visione più approfondita leggi gli articoli alla fine).

L'idea nei GC generazionali è che il più delle volte devono essere considerate solo le giovani generazioni. La posizione principale viene scansionata per i riferimenti, quindi vengono scansionati gli oggetti della nuova generazione. Durante queste spazzate più frequenti non viene controllato alcun oggetto della vecchia generazione.

Ora, il problema deriva dal fatto che un oggetto non può avere riferimenti a oggetti più giovani. Quando un oggetto di lunga durata (vecchia generazione) ottiene un riferimento a un nuovo oggetto, tale riferimento deve essere tracciato in modo esplicito dal garbage collector (vedere l'articolo di IBM sull'hotspot JVM collector ), influendo effettivamente sulle prestazioni del GC.

Il motivo per cui un oggetto vecchio non può riferirsi a uno più giovane è che, poiché il vecchio oggetto non viene controllato in raccolte minori, se l'unico riferimento all'oggetto è mantenuto nel vecchio oggetto, non verrà contrassegnato e sarebbe errato deallocato durante la fase di sweep.

Ovviamente, come sottolineato da molti, la parola chiave final non influisce realmente sul garbage collector, ma garantisce che il riferimento non verrà mai cambiato in un oggetto più giovane se questo oggetto sopravvive alle raccolte minori e arriva all'heap più vecchio.

Articoli:

IBM sulla garbage collection: storia , nella JVM hotspot e prestazioni . Questi potrebbero non essere più completamente validi, poiché risalgono al 2003/04, ma forniscono alcune informazioni di facile lettura sui GC.

Sun on Tuning garbage collection


3

GC agisce su riferimenti irraggiungibili. Questo non ha nulla a che fare con "finale", che è semplicemente un'affermazione di assegnazione una tantum. È possibile che alcuni GC di VM possano utilizzare "finale"? Non vedo come o perché.


3

finalsu variabili e parametri locali non fa differenza per i file di classe prodotti, quindi non può influire sulle prestazioni di runtime. Se una classe non ha sottoclassi, HotSpot tratta quella classe come se fosse comunque finale (può essere annullata in seguito se viene caricata una classe che infrange tale presupposto). Credo che i finalmetodi siano più o meno gli stessi delle classi. finalsu un campo statico può consentire alla variabile di essere interpretata come una "costante del tempo di compilazione" e l'ottimizzazione deve essere eseguita da javac su tale base. finalon fields consente alla JVM una certa libertà di ignorare le relazioni che accadono prima .


2

Sembra che ci siano molte risposte che sono congetture vaganti. La verità è che non esiste un modificatore finale per le variabili locali a livello di bytecode. La macchina virtuale non saprà mai che le tue variabili locali sono state definite come finali o meno.

La risposta alla tua domanda è un deciso no.


Potrebbe essere vero, ma il compilatore può comunque utilizzare le informazioni finali durante l'analisi del flusso di dati.

@WernerVanBelle il compilatore sa già che una variabile viene impostata una sola volta. Deve già eseguire l'analisi del flusso di dati per sapere che una variabile potrebbe essere nulla, non è inizializzata prima dell'uso, ecc. Quindi, le finali locali non offrono alcuna nuova informazione al compilatore.
Matt Quigley

Non è così. L'analisi del flusso di dati può dedurre molte cose, ma è possibile avere un programma completo di turing all'interno di un blocco che imposterà o meno una variabile locale. Il compilatore non può sapere in anticipo se la variabile verrà scritta e sarà affatto costante. Pertanto il compilatore, senza la parola chiave final, non può garantire che una variabile sia final o meno.

@WernerVanBelle Sono sinceramente incuriosito, puoi fare un esempio? Non vedo come possa esserci un'assegnazione finale a una variabile non finale di cui il compilatore non è a conoscenza. Il compilatore sa che se hai una variabile non inizializzata, non ti permetterà di usarla. Se provi ad assegnare una variabile finale all'interno di un ciclo, il compilatore non te lo consente. Qual è un esempio in cui una variabile che PU essere dichiarata come finale, ma NON lo è, e il compilatore non può garantire che sia finale? Ho il sospetto che qualsiasi esempio sarebbe una variabile che non può essere dichiarata come finale in primo luogo.
Matt Quigley

Ah dopo aver riletto il tuo commento, vedo che il tuo esempio è una variabile inizializzata all'interno di un blocco condizionale. Quelle variabili non possono essere definitive in primo luogo, a meno che tutti i percorsi delle condizioni inizializzino la variabile una volta. Quindi, il compilatore conosce tali dichiarazioni: è così che ti permette di compilare una variabile finale inizializzata in due posti diversi (pensa final int x; if (cond) x=1; else x=2;). Il compilatore, senza la parola chiave final, quindi può garantire che una variabile sia final o meno.
Matt Quigley

1

Tutti i metodi e le variabili possono essere sovrascritti per impostazione predefinita nelle sottoclassi. Se vogliamo salvare le sottoclassi dall'override dei membri della superclasse, possiamo dichiararle come finali usando la parola chiave final. Ad esempio, final int a=10; final void display(){......} rendere definitivo un metodo garantisce che la funzionalità definita nella superclasse non verrà mai modificata comunque. Allo stesso modo il valore di una variabile finale non può mai essere modificato. Le variabili finali si comportano come variabili di classe.


1

A rigor di termini , i campi di istanza final potrebbero migliorare leggermente le prestazioni se un particolare GC desidera sfruttarlo. Quando si verifica un evento simultaneo GC(ciò significa che l'applicazione è ancora in esecuzione, mentre GC è in corso ), vedere questo per una spiegazione più ampia , i GC devono impiegare determinate barriere quando le operazioni di scrittura e / o lettura vengono eseguite. Il collegamento che ti ho fornito lo spiega più o meno, ma per renderlo davvero breve: quando a GCfa del lavoro simultaneo, tutte le letture e le scritture nell'heap (mentre quel GC è in corso), vengono "intercettate" e applicate più tardi nel tempo; in modo che la fase GC simultanea possa finire il suo lavoro.

Ad finalesempio i campi, poiché non possono essere modificati (salvo riflessione), queste barriere possono essere omesse. E questa non è solo pura teoria.

Shenandoah GCli ha in pratica (anche se non per molto ), e puoi fare, ad esempio:

-XX:+UnlockExperimentalVMOptions  
-XX:+UseShenandoahGC  
-XX:+ShenandoahOptimizeInstanceFinals

E ci saranno ottimizzazioni nell'algoritmo GC che lo renderanno leggermente più veloce. Questo perché non ci saranno barriere intercettabili final, poiché nessuno dovrebbe modificarle, mai. Nemmeno tramite riflessione o JNI.


0

L'unica cosa a cui riesco a pensare è che il compilatore potrebbe ottimizzare le variabili finali e incorporarle come costanti nel codice, così si finisce senza memoria allocata.


0

assolutamente, fintanto che si accorcia la vita dell'oggetto, il che offre un grande vantaggio della gestione della memoria, recentemente abbiamo esaminato la funzionalità di esportazione con variabili di istanza su un test e un altro test con variabili locali a livello di metodo. durante il test di carico, JVM genera un errore di memoria al primo test e JVM viene interrotto. ma nel secondo test, in grado di ottenere il rapporto con successo grazie a una migliore gestione della memoria.


0

L'unica volta che preferisco dichiarare le variabili locali come finali è quando:

  • Io ho per renderli così finale che possono essere condivise con qualche classe anonima (ad esempio: creazione thread demone e lasciarlo accesso qualche valore dal metodo racchiude)

  • Io voglio farli finale (ad esempio: un valore che non dovrebbe / non viene sovrascritto per errore)

Aiutano nella raccolta rapida dei rifiuti?
Per quanto ne so un oggetto diventa un candidato della raccolta GC se ha zero riferimenti forti ad esso e anche in questo caso non vi è alcuna garanzia che venga immediatamente garbage collection. In generale, si dice che un riferimento forte muoia quando esce dall'ambito o l'utente lo riassegna esplicitamente a riferimento nullo, quindi, dichiararli finali significa che il riferimento continuerà a esistere fino a quando il metodo non esiste (a meno che il suo ambito non sia esplicitamente ristretto a un blocco interno specifico {}) perché non è possibile riassegnare le variabili finali (ovvero non è possibile riassegnare a null). Quindi penso che rispetto a Garbage Collection "finale" possa introdurre un possibile ritardo indesiderato, quindi bisogna stare un po 'attenti nel definire lo scopo in quanto controlla quando diventeranno candidati per GC.

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.