2013-09-05 3 views
7

Basta Lasciatemi cominciare con una dimostrazione:Perché l'esistenza di un blocco try/finally interrompe il funzionamento del garbage collector?

[TestMethod] 
public void Test() 
{ 
    var h = new WeakReference(new object()); 
    GC.Collect(); 
    Assert.IsNull(h.Target); 
} 

Questo codice funziona come previsto. Dopo che la garbage collection è finita, il riferimento in h è annullato. Ora, ecco il colpo di scena:

[TestMethod] 
public void Test() 
{ 
    var h = new WeakReference(new object()); 
    GC.Collect(); 
    try { }  // I just add an empty 
    finally { } // try/finally block 
    Assert.IsNull(h.Target); // FAIL! 
} 

aggiungo un vuoto try/finally per il test dopo la linea GC.Collect() ed ecco, l'oggetto debolmente fa riferimento non sono raccolte! Se il blocco try/finally vuoto viene aggiunto prima del la riga GC.Collect(), il test passa comunque.

Cosa dà? Qualcuno può spiegare come esattamente i blocchi try/finally influenzano la durata degli oggetti?

Nota: tutti i test eseguiti in Debug. In Release entrambi i test passano.

Nota 2: per riprodurre l'app deve utilizzare come destinazione .NET 4 o il runtime .NET 4.5 e deve essere eseguito come 32 bit (o destinazione x86, o Qualsiasi CPU con opzione "Prefer 32-bit" controllato)

+1

Non riproducibile su un programma di console equivalente (VS 2010, sia per il debug che per il rilascio) – xanatos

+0

Si può verificare di nuovo per favore? Sono stato in grado di riprodurlo con VS2010 e VS2012 nel programma di console equivalente, in Debug. –

+0

Forse dipende dalle subreleases della macchina .NET – xanatos

risposta

3

Quando un debugger è collegato, il jitter modifica la durata delle variabili locali. Spiegato in dettaglio in this answer. In breve, senza un debugger la vita termina con l'ultimo utilizzo della variabile nel codice, con un debugger viene esteso alla fine del metodo per consentire a un'espressione di debugger di Watch di funzionare.

Mentre sembra come l'espressione new object() non viene memorizzato in una variabile nel codice, c'è ancora uno dopo il generatore di codice jitter è fatto con esso. Il riferimento all'oggetto è memorizzato nello stack frame su [ebp-44h], indistinguibile dal modo in cui verrebbe utilizzata una variabile locale. L'unico modo che puoi vedere è guardando il codice macchina generato, usa Debug + Windows + Disassembly. Questo è altrimenti del tutto normale, questo tipo di memorie di memoria ridondanti viene eliminato dall'ottimizzatore del jitter ma non è abilitato nel build di Debug.

Anche se è temporaneo, questa variabile deve ancora essere segnalata al GC come archivio di un riferimento. Necessario per impedire che l'oggetto venga raccolto quando un GC si verifica esattamente tra la chiamata del costruttore dell'oggetto e la chiamata del costruttore WeakReference. Possibile se un altro thread nel programma attiva una raccolta.

Senza i blocchi try/finally, il jitter può ancora scoprire che lo slot del frame dello stack memorizza un temporaneo e non è effettivamente necessario estenderne la durata. Quindi smette di riportare la durata del temporaneo prima della chiamata a GC.Collect() e l'oggetto viene raccolto.

Ma con i blocchi try/finally, il jitter si interrompe cercando di capire se c'è un possibile utilizzo dello slot del frame stack nei blocchi try o finally. E punisce il problema semplicemente estendendo la sua durata alla fine del metodo, come accadrebbe con una normale variabile locale.

Questo è tutto piuttosto normale, semplicemente non è possibile formulare alcuna ipotesi ragionevole sul modo in cui i riferimenti alle variabili locali vengono trattati in codice non ottimizzato. Questo dovrebbe anche essere un forte avvertimento per chiunque usi effettivamente un TestMethod eseguito in un tester di unità, mai testare il build di codice di Debug, solo la build di Release. Semplicemente non si comporterà come il modo in cui funziona sulla macchina dell'utente.

+0

Quindi sembra che il JIT x86 sia un po 'di salsa debole. Questo problema non si verifica con JIT x64 o JIT .NET 2.0 - è apparentemente un deficit JIT (o bug di regressione). –

+0

Il jitter non ha l'obbligo di generare codice ottimizzato quando si disattiva intenzionalmente l'ottimizzatore. Il suo unico requisito è generare codice corretto e debuggabile. È possibile utilizzare connect.microsoft.com per segnalare una regressione, ma prevedo un rapido "non risolverà". Questo non ha bisogno di essere risolto. –

0

Per semplificare il debug, in modalità di debug gli oggetti dichiarati localmente non vengono eliminati. Mentre io non ero in grado di riprodurre il problema, con questo codice:

var x = new object(); 
var h = new WeakReference(x); 
GC.Collect(); 
try { }  // I just add an empty 
finally { } // try/finally block 
Console.WriteLine(h.Target != null); 
Console.ReadKey(); 

ero in grado di riprodurre il problema. Se GC.Collect() è stato in grado di "raccogliere" lo new object(), se si inserisce un punto di interruzione dopo lo Console.ReadKey(), sarà possibile visualizzare un oggetto già disposto (x).

Qualcuno aveva chiesto qualcosa di simile qui: https://stackoverflow.com/a/755688/613130

Un commento è interessante:

A causa di ottimizzazioni in modalità di rilascio, l'ambito di riferimento è valida solo fino al suo ultimo utilizzo e non l'intero blocco di il codice è definito in.

chiaramente in modalità di debug è l'opposto, un ambito di riferimento è l'intero ambito in cui è definito.

+0

Sì, capisco perché il tuo frammento riproduce il problema (durata di 'x' estesa alla fine dell'ambito per le build di debug). Tuttavia, non posso considerare il tuo post una risposta, perché ero ancora in grado di riprodurre il problema all'interno di un'app console con lo snippet originale che non ha una variabile 'x', quindi non ci sono problemi di scope. –