2012-01-11 7 views
11

Ho riscontrato dei problemi con WeakHashMap.WeakHashMap è stato cancellato durante un GC completo?

Considerate questo codice di esempio:

List<byte[]> list = new ArrayList<byte[]>(); 

Map<String, Calendar> map = new WeakHashMap<String, Calendar>(); 
String anObject = new String("string 1"); 
String anOtherObject = new String("string 2"); 

map.put(anObject, Calendar.getInstance()); 
map.put(anOtherObject, Calendar.getInstance()); 
// In order to test if the weakHashMap works, i remove the StrongReference in this object 
anObject = null; 
int i = 0; 
while (map.size() == 2) { 
    byte[] tab = new byte[10000]; 
    System.out.println("iteration " + i++ + "map size :" + map.size()); 
    list.add(tab); 
} 
System.out.println("Map size " + map.size()); 

questo codice funziona. All'interno dei loop, sto creando un oggetto. Quando si verifica un GC secondario, la dimensione della mappa è uguale a 1 alla 1360a iterazione. Va tutto bene.

Ora, quando io commento questa linea:

//anObject = null; 

mi aspetto di avere un OutOfMemoryError perché il mapSize è sempre uguale a 2. Tuttavia al esima iterazione 26XXX, si verifica un GC pieno e la dimensione della mappa è uguale a 0. Non capisco perché?

Ho pensato che la mappa non avrebbe dovuto essere cancellata perché ci sono anche forti riferimenti a entrambi gli oggetti.

+0

Penso che il test non sia corretto. Se si cambia 'while (map.size() == 2) {' a 'while (map.size()> 0) {', i due test terminano entrambi finché la mappa non è vuota, indipendentemente dal commento 'anObject = null' o no. A proposito, l'ho già provato. – donnior

+0

Stampa 'anObject' e' anOtherObject' alla fine. Il compilatore vede che non li stai più utilizzando e puoi rimuoverli prima. –

risposta

10

Il just-in-time compilatore analizza il codice, vede che anObject e anOtherObject non sono utilizzati dopo il ciclo, e li rimuove dalla tabella delle variabili locali o li mette a null, mentre il ciclo è ancora in esecuzione. Questa è chiamata compilazione OSR.

In seguito il GC raccoglie le stringhe perché non rimangono riferimenti forti a esse.

Se si utilizzava anObject dopo il ciclo, si ottiene comunque un OutOfMemoryError.

Aggiornamento: Troverete una discussione più dettagliata su OSR compilation nel mio blog.

+0

Penso che tu abbia esattamente ragione - ma non si tratta forse di un ottimizzazione JIT potenzialmente distruttiva? Se 'anObject' ha un finalizzatore ed è GC'd prima che il riferimento scompaia, il finalizzatore verrà eseguito potenzialmente prima che lo si intenda. – berry120

+0

Cosa potrebbe rompere? Quando viene eseguito il finalizzatore, il riferimento forte non esiste più. – Joni

+0

Potrebbe rompersi nel senso in cui potrebbe eseguire un finalizzatore prima del previsto; prima che l'hard reference sia effettivamente andato fuori uso. – berry120

7

Bit di scavo rivela che questo è esplicitamente trattato nel JLS, sezione 12.6.1:

Ottimizzazione trasformazioni di un programma possono essere progettati che riducono il numero di oggetti che sono raggiungibili essere inferiori a quelli che sarebbe ingenuamente considerato raggiungibile. Ad esempio, un compilatore o un generatore di codice può scegliere di impostare una variabile o un parametro che non sarà più utilizzato per null per causare l'archiviazione per tale oggetto potenzialmente recuperabile prima.

(Bolding è la mia aggiunta.)

http://java.sun.com/docs/books/jls/third_edition/html/execution.html#12.6.1

Quindi, in sostanza, il JIT è permesso di rimuovere i riferimenti forti ogni volta che vuole, se si può capire che non potranno mai essere utilizzati di nuovo - che è esattamente ciò che sta accadendo qui.

Questa è una grande domanda e rappresenta un grande puzzle che può essere facilmente mostrato solo perché un oggetto sembra avere un forte riferimento nell'ambito, non significa necessariamente che non è stato raccolto. In seguito a ciò, significa che non puoi garantire esplicitamente nulla su quando verrà eseguito un finalizzatore, potrebbe anche essere nel caso in cui sembra che l'oggetto sia ancora in uso!

Es:

List<byte[]> list = new ArrayList<byte[]>(); 

Object thing = new Object() { 
    protected void finalize() { 
     System.out.println("here"); 
    } 
}; 
WeakReference<Object> ref = new WeakReference<Object>(thing); 

while(ref.get()!=null) { 
    list.add(new byte[10000]); 
} 
System.out.println("bam"); 

Quanto sopra è un esempio semplice che mostra l'oggetto viene terminato e GC'd prima anche se il riferimento thing esiste ancora

7
(qui viene stampata, quindi BAM.)

Solo per aggiungere una piccola cosa alle eccellenti risposte di Joni Salonen e berry120. Si può dimostrare che il JIT è in realtà il responsabile della "rimozione variabile" semplicemente spegnendolo con -Djava.compiler=NONE. Una volta spento, ottieni l'OOME.

Se vogliamo sapere cosa sta succedendo sotto i cofani, l'opzione XX:+PrintCompilation mostra l'attività JIT. Usandolo con il codice dalla questione l'uscita si ottiene è la seguente:

1  java.lang.String::hashCode (64 bytes) 
2  java.lang.String::charAt (33 bytes) 
3  java.lang.String::indexOf (151 bytes) 
4  java.util.ArrayList::add (29 bytes) 
5  java.util.ArrayList::ensureCapacity (58 bytes) 
6 ! java.lang.ref.ReferenceQueue::poll (28 bytes) 
7  java.util.WeakHashMap::expungeStaleEntries (125 bytes) 
8  java.util.WeakHashMap::size (18 bytes) 
1%  WeakHM::main @ 63 (126 bytes) 
Map size 0 

L'ultima compilazione (con la bandiera @) è un OSR (On sostituzione Stack) compilazione (controllare https://gist.github.com/1165804#file_notes.md per ulteriori dettagli). In parole semplici, consente alla VM di sostituire un metodo mentre è in esecuzione e viene utilizzato per migliorare le prestazioni dei metodi Java bloccati nei loop. Direi che dopo che questa compilazione è stata attivata, il JIT rimuove le variabili che non vengono più utilizzate.