2014-10-29 13 views
16

Abbiamo recentemente aggiornato la nostra applicazione di elaborazione dei messaggi da Java 7 a Java 8. Dall'aggiornamento, si ottiene un'eccezione occasionale che un flusso è stato chiuso durante la lettura. La registrazione mostra che il thread del finalizzatore chiama finalize() sull'oggetto che contiene lo stream (che a sua volta chiude lo stream).finalize() chiamato su oggetto fortemente raggiungibile in Java 8

Lo schema di base del codice è il seguente:

MIMEWriter writer = new MIMEWriter(out); 
in = new InflaterInputStream(databaseBlobInputStream); 
MIMEBodyPart attachmentPart = new MIMEBodyPart(in); 
writer.writePart(attachmentPart); 

MIMEWriter e MIMEBodyPart sono parte di una libreria/HTTP MIME home-grown. MIMEBodyPart estende HTTPMessage, che ha la seguente:

public void close() throws IOException 
{ 
    if (m_stream != null) 
    { 
     m_stream.close(); 
    } 
} 

protected void finalize() 
{ 
    try 
    { 
     close(); 
    } 
    catch (final Exception ignored) { } 
} 

L'eccezione si verifica nella catena invocazione di MIMEWriter.writePart, che è la seguente:

  1. MIMEWriter.writePart() scrive le intestazioni per la parte, poi chiama part.writeBodyPartContent(this)
  2. MIMEBodyPart.writeBodyPartContent() chiama il nostro metodo di utilità IOUtil.copy(getContentStream(), out) per lo streaming del contenuto nell'output
  3. MIMEBodyPart.getContentStream() jus t restituisce il flusso di input passato al contstructor (vedere il blocco di codice sopra)
  4. IOUtil.copy ha un ciclo che legge un blocco di 8K dal flusso di input e lo scrive nel flusso di output finché il flusso di input non è vuoto.

Il MIMEBodyPart.finalize() viene chiamato mentre IOUtil.copy è in esecuzione, e si ottiene la seguente eccezione:

java.io.IOException: Stream closed 
    at java.util.zip.InflaterInputStream.ensureOpen(InflaterInputStream.java:67) 
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:142) 
    at java.io.FilterInputStream.read(FilterInputStream.java:107) 
    at com.blah.util.IOUtil.copy(IOUtil.java:153) 
    at com.blah.core.net.MIMEBodyPart.writeBodyPartContent(MIMEBodyPart.java:75) 
    at com.blah.core.net.MIMEWriter.writePart(MIMEWriter.java:65) 

Abbiamo messo un po 'di registrazione nel metodo HTTPMessage.close() che ha registrato la traccia dello stack del chiamante e ha dimostrato che si tratta di sicuramente il thread di finalizzazione che sta invocando HTTPMessage.finalize() mentre IOUtil.copy() è in esecuzione.

L'oggetto MIMEBodyPart è definitivamente raggiungibile dallo stack del thread corrente come this nello stack frame per MIMEBodyPart.writeBodyPartContent. Non capisco perché la JVM chiamerebbe finalize().

Ho tentato di estrarre il codice relativo e di eseguirlo in un ciclo chiuso sulla mia macchina, ma non riesco a riprodurre il problema. Possiamo riprodurre in modo affidabile il problema con un carico elevato su uno dei nostri server di sviluppo, ma qualsiasi tentativo di creare un caso di test riproducibile più piccolo ha avuto esito negativo. Il codice è compilato sotto Java 7 ma viene eseguito sotto Java 8. Se si passa a Java 7 senza ricompilare, il problema non si verifica.

Come soluzione temporanea, ho riscritto il codice interessato utilizzando la libreria MIME di Java Mail e il problema è scomparso (presumibilmente Java Mail non utilizza finalize()). Tuttavia, sono preoccupato che altri metodi finalize() nell'applicazione possano essere chiamati in modo errato o che Java stia cercando di raccogliere gli oggetti che sono ancora in uso.

So che l'attuale best practice consiglia di non utilizzare finalize() e probabilmente esaminerò questa libreria cresciuta in proprio per rimuovere i metodi finalize(). Detto questo, qualcuno ha mai incontrato prima questo problema? Qualcuno ha qualche idea sulla causa?

+2

Sarei sorpreso se non ci fosse altra spiegazione a questo. Il thread corrente è sempre una "radice" per il collector per identificare gli oggetti live. Come sai per certo che il finalizzatore è stato invocato * prima * il tuo IOUtils.copy() restituisce? – Bhaskar

+0

Sembra molto simile a un bug JIT. Vorrei correre con il debug JIT attivato e vedere se ci sono dei pattern lì. – chrylis

+0

@Bhaskar, l'eccezione dimostra che il flusso viene chiuso mentre è in esecuzione 'IOUtil.copy()'. – Nathan

risposta

19

Un po 'di congettura qui. È possibile che un oggetto sia finalizzato e garbage collection anche se ci sono riferimenti ad esso in variabili locali nello stack, e anche se c'è una chiamata attiva a un metodo di istanza di quell'oggetto nello stack! Il requisito è che l'oggetto sia non raggiungibile. Anche se è in pila, se nessun codice successivo tocca quel riferimento, è potenzialmente irraggiungibile.

Vedere this other answer per un esempio di come un oggetto può essere sottoposto a GC mentre una variabile locale che fa riferimento a tale oggetto è ancora in ambito.

Ecco un esempio di come un oggetto può essere finalizzato durante una chiamata metodo di istanza è attiva:

class FinalizeThis { 
    protected void finalize() { 
     System.out.println("finalized!"); 
    } 

    void loop() { 
     System.out.println("loop() called"); 
     for (int i = 0; i < 1_000_000_000; i++) { 
      if (i % 1_000_000 == 0) 
       System.gc(); 
     } 
     System.out.println("loop() returns"); 
    } 

    public static void main(String[] args) { 
     new FinalizeThis().loop(); 
    } 
} 

Mentre il metodo loop() è attivo, non c'è alcuna possibilità di qualsiasi codice di fare qualsiasi cosa con il riferimento al FinalizeThis oggetto, quindi è irraggiungibile. E quindi può essere finalizzato e GC'ed. Su JDK 8 GA, viene stampato quanto segue:

loop() called 
finalized! 
loop() returns 

ogni volta.

Qualcosa di simile potrebbe succedere con MimeBodyPart. Viene memorizzato in una variabile locale? (Sembra di sì, dato che il codice sembra aderire ad una convenzione che i campi sono denominati con un prefisso m_.)

UPDATE

Nei commenti, l'OP suggerito fare la seguente modifica:

public static void main(String[] args) { 
     FinalizeThis finalizeThis = new FinalizeThis(); 
     finalizeThis.loop(); 
    } 

Con questa modifica non ha rispettato finalizzazione, e nemmeno io Tuttavia, se questo ulteriore cambiamento è fatto:

public static void main(String[] args) { 
     FinalizeThis finalizeThis = new FinalizeThis(); 
     for (int i = 0; i < 1_000_000; i++) 
      Thread.yield(); 
     finalizeThis.loop(); 
    } 

finalizzazione si verifica ancora una volta. Sospetto che il motivo sia che senza il ciclo, il metodo main() è interpretato, non compilato. L'interprete è probabilmente meno aggressivo sull'analisi della raggiungibilità. Con il ciclo di rendimento in atto, viene compilato il metodo main() e il compilatore JIT rileva che finalizeThis è diventato irraggiungibile mentre è in esecuzione il metodo loop().

Un altro modo per attivare questo comportamento è utilizzare l'opzione -Xcomp per la JVM, che forza i metodi per essere compilati JIT prima dell'esecuzione. Non avrei eseguito un'intera applicazione in questo modo - la compilazione JIT di tutto può essere piuttosto lenta e richiedere molto spazio - ma è utile per svuotare casi come questo in piccoli programmi di test, invece di armeggiare con loop.

+2

Grazie @Stuart Marks: hai ripristinato la mia fiducia in Stack Overflow. Il pensiero interessante del tuo programma è che mi dà lo stesso su Java 7 e Java 8. Il mio problema è apparso solo quando siamo passati a Java 8. La tua analisi ha comunque un senso e mi ispira ulteriormente a rimuovere 'finalize()' dal nostro codice base. – Nathan

+0

Un'altra query: ho modificato il metodo principale nel tuo programma in FinalizeThis finalizeThis = new FinalizeThis(); finalizeThis.loop(); '. Una volta che l'ho fatto, l'oggetto non è mai stato finalizzato. Non sono sicuro del perché sarebbe diverso. Hai qualche idea? – Nathan

+2

@Nathan Ho aggiornato la mia risposta in risposta al tuo commento. Non sei sicuro del motivo per cui vedi il problema nella tua app su Java 8. Un gruppo di euristiche JIT probabilmente è cambiato tra 7 e 8, il che potrebbe essere responsabile di differenze di comportamento come questa. Inoltre, se rimuovi il finalizzatore, assicurati che qualcosa finisca per chiudere il flusso. Forse usare try-with-resources. –

2

Il tuo finalizzatore non è corretto.

In primo luogo, non ha bisogno del blocco catch e deve chiamare super.finalize() nel proprio blocco finally{}. La forma canonica di un finalizzatore è la seguente:

protected void finalize() throws Throwable 
{ 
    try 
    { 
     // do stuff 
    } 
    finally 
    { 
     super.finalize(); 
    } 
} 

In secondo luogo, si sta assumendo si tiene l'unico riferimento a m_stream, che può o non può essere corretta. Il membro m_stream dovrebbe finalizzarsi. Ma non è necessario fare nulla per ottenere ciò. In definitiva, il m_stream sarà un FileInputStream o FileOutputStream o un flusso di socket e sono già finalizzati correttamente.

Vorrei semplicemente rimuoverlo.

+0

Concordo in linea di principio sulla chiamata di 'super.finalize()' ma in questo caso 'HTTPMessage' estende' Object' che ha un 'finalize()' vuoto. Sono d'accordo sul fatto che il codice così com'è non sia ottimale, ma non sono nemmeno sicuro che sia rilevante. Il problema è che 'finalize()' sembra essere chiamato in modo errato sotto Java 8 ma non era sotto Java 7. – Nathan

+0

Lo rimuoverò ancora e vedrò cosa succede. Non ne hai bisogno ed è una possibile fonte di errore per tutti i motivi che ho affermato. – EJP

+0

Sono d'accordo che il 'finalize()' non è necessario in questo caso (chiamiamo 'in.close()' direttamente più in basso nel codice). Tuttavia, la nostra libreria HTTP/MIME è utilizzata in altri scenari in cui 'finalize()' potrebbe essere necessario. Come ho detto, rivisiterò la libreria e tutti i suoi usi in futuro per rimuovere 'finalize()'. Ho anche già riscritto il codice in questione per evitare il problema. Abbiamo altre aree del nostro codice che hanno anche metodi 'finalize()'. La mia preoccupazione è che potremmo imbattersi in questo stesso problema in altre parti del nostro codice. – Nathan

Problemi correlati