2014-10-29 11 views
16

Wir haben unsere Nachrichtenverarbeitungsanwendung kürzlich von Java 7 auf Java 8 aktualisiert. Seit dem Upgrade erhalten wir gelegentlich die Ausnahme, dass ein Stream geschlossen wurde, während er gelesen wird. Die Protokollierung zeigt, dass der Finalizer-Thread finalize() für das Objekt aufruft, das den Stream enthält (wodurch wiederum der Stream geschlossen wird).finalize() hat stark erreichbares Objekt in Java aufgerufen 8

Die Grundzüge des Codes ist wie folgt:

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

MIMEWriter und MIMEBodyPart Teil eines home-grown MIME/HTTP-Bibliothek sind. MIMEBodyPart erstreckt HTTPMessage, welches die folgenden:

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

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

Die Ausnahme tritt in der Aufrufkette MIMEWriter.writePart, die wie folgt lautet:

  1. MIMEWriter.writePart() die Header für den Teil schreibt, dann ruft part.writeBodyPartContent(this)
  2. MIMEBodyPart.writeBodyPartContent() ruft unsere Hilfsmethode IOUtil.copy(getContentStream(), out) auf, um den Inhalt zum Ausgang zu senden
  3. MIMEBodyPart.getContentStream() jus t gibt den an den contstructor übergebenen Eingabestrom zurück (siehe obigen Codeblock)
  4. IOUtil.copy verfügt über eine Schleife, die einen 8K Chunk aus dem Eingabestream liest und in den Ausgabestream schreibt, bis der Eingabestream leer ist.

Die MIMEBodyPart.finalize() heißt während IOUtil.copy ausgeführt wird, und es wird die folgende Ausnahme:

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) 

Wir einige Protokollierung setzen in der HTTPMessage.close() Methode, die den Stack-Trace des Anrufers aufgezeichnet und bewiesen, dass es definitiv der Finalizer-Thread, der HTTPMessage.finalize() aufruft, während IOUtil.copy() läuft.

Das MIMEBodyPart Objekt ist definitiv vom Stack des aktuellen Threads als this im Stack-Frame für MIMEBodyPart.writeBodyPartContent erreichbar. Ich verstehe nicht, warum die JVM finalize() anrufen würde.

Ich habe versucht, den relevanten Code zu extrahieren und es in einer engen Schleife auf meinem eigenen Rechner laufen zu lassen, aber ich kann das Problem nicht reproduzieren. Wir können das Problem mit hoher Last auf einem unserer Dev-Server zuverlässig reproduzieren, aber alle Versuche, einen kleineren reproduzierbaren Testfall zu erstellen, sind fehlgeschlagen. Der Code wird unter Java 7 kompiliert, aber unter Java 8 ausgeführt. Wenn wir ohne Neukompilierung zurück zu Java 7 wechseln, tritt das Problem nicht auf.

Als Workaround habe ich den betroffenen Code mit der Java Mail MIME-Bibliothek neu geschrieben und das Problem ist weg (vermutlich verwendet Java Mail nicht finalize()). Ich bin jedoch besorgt, dass andere Methoden in der Anwendung finalize() möglicherweise falsch aufgerufen werden, oder dass Java versucht, Objekte zu sammeln, die noch verwendet werden.

Ich weiß, dass aktuelle Best Practice empfiehlt gegen die Verwendung finalize() und ich werde wahrscheinlich diese selbstgewachsene Bibliothek wieder zu entfernen, um die finalize() Methoden zu entfernen. Hat jemand schon einmal auf dieses Problem gestoßen? Hat jemand irgendwelche Ideen bezüglich der Ursache?

+2

Ich wäre überrascht, wenn es keine andere Erklärung dafür gibt. Der aktuelle Thread ist immer eine "Wurzel" für den Kollektor, um Live-Objekte zu identifizieren. Wie können Sie sicher sein, dass der Finalizer aufgerufen wird * bevor * Ihre IOUtils.copy() zurückgibt? – Bhaskar

+0

Das klingt sehr nach einem JIT-Bug. Ich würde laufen mit JIT-Debugging eingeschaltet und sehen, ob dort irgendein Muster ist. – chrylis

+0

@Bhaskar, die Ausnahme beweist, dass der Stream geschlossen ist, während 'IOUtil.copy()' ausgeführt wird. – Nathan

Antwort

19

Ein bisschen Mutmaßung hier. Es ist möglich, dass ein Objekt finalisiert und Speicherbereinigung durchgeführt wird, auch wenn Verweise darauf in lokalen Variablen auf dem Stapel vorhanden sind, und sogar wenn ein Aufruf einer Instanzmethode dieses Objekts auf dem Stapel aktiv ist! Voraussetzung ist, dass das Objekt nicht erreichbar ist. Selbst wenn es sich auf dem Stapel befindet, wenn kein nachfolgender Code diese Referenz berührt, ist es möglicherweise nicht erreichbar.

Siehe this other answer für ein Beispiel, wie ein Objekt GC'ed werden kann, während eine lokale Variable, die darauf verweist, immer noch im Bereich ist.

Hier ist ein Beispiel dafür, wie ein Objekt fertig gestellt werden, während eine Instanz Methodenaufruf ist aktiv:

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(); 
    } 
} 

Während die loop() Methode aktiv ist, gibt es keine Möglichkeit eines Codes mit dem Verweis auf die etwas zu tun FinalizeThis Objekt, also ist es nicht erreichbar. Und deshalb kann es abgeschlossen und GC'ed sein. Am JDK 8 GA wird dabei Folgendes gedruckt:

jedes Mal.

Etwas ähnliches könnte mit MimeBodyPart weitergehen. Wird es in einer lokalen Variablen gespeichert? (Es scheint so, da der Code auf eine Konvention zu halten scheint, die Felder mit einem m_ Präfix genannt werden.)

UPDATE

In den Kommentaren, die OP vorgeschlagen, die folgende Änderung vorgenommen:

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

Mit dieser Änderung er nicht Finalisierung beobachten ist, und auch nicht, aber I. wenn diese weitere Änderung vorgenommen wird:

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

Finalisierung erneut auftritt. Ich vermute, der Grund ist, dass ohne die Schleife die main()-Methode interpretiert wird, nicht kompiliert. Der Interpreter ist wahrscheinlich weniger aggressiv bezüglich der Erreichbarkeitsanalyse. Wenn die Yield-Schleife vorhanden ist, wird die main()-Methode kompiliert, und der JIT-Compiler erkennt, dass finalizeThis nicht mehr erreichbar ist, während die loop()-Methode ausgeführt wird.

Eine andere Möglichkeit, dieses Verhalten auszulösen, ist die Verwendung der Option -Xcomp für die JVM, die vor der Ausführung erzwingt, dass Methoden JIT-kompiliert werden. Ich würde nicht eine ganze Anwendung auf diese Weise ausführen - JIT-Compiling alles kann ziemlich langsam und viel Platz nehmen - aber es ist nützlich, Fälle wie diese in kleinen Testprogrammen auszuwaschen, anstatt an Schleifen zu basteln.

+2

Dank @Stuart Marks - Sie haben mein Vertrauen in Stack Overflow wiederhergestellt. Das Interessante an Ihrem Programm ist, dass es mir dasselbe auf Java 7 und Java 8 gibt. Mein Problem tauchte erst auf, als wir zu Java 8 gewechselt haben. Ihre Analyse macht jedoch Sinn und inspiriert mich weiter, 'finalize()' von unserem zu entfernen Codebasis. – Nathan

+0

Eine andere Abfrage - Ich habe die Hauptmethode in Ihrem Programm auf 'FinalizeThis finalizeThis = new FinalizeThis(); finalizeThis.loop(); '. Sobald ich das getan habe, wurde das Objekt nie fertiggestellt. Ich bin mir nicht sicher, warum das anders wäre. Hast du eine Idee? – Nathan

+2

@Nathan Ich habe meine Antwort als Antwort auf Ihren Kommentar aktualisiert. Nicht sicher, warum Sie das Problem nur in Ihrer App auf Java 8 sehen. Eine Menge JIT-Heuristiken hat sich wahrscheinlich zwischen 7 und 8 geändert, was für Verhaltensunterschiede wie diese verantwortlich sein könnte. Wenn Sie den Finalizer entfernen, stellen Sie außerdem sicher, dass der Stream schließlich geschlossen wird. Verwenden Sie vielleicht Try-with-Resources. –

2

Ihr Finalizer ist nicht korrekt.

Erstens braucht es nicht den catch-Block, und es muss super.finalize() in seinem eigenen finally{} Block aufrufen. Die kanonische Form eines Finalizerthread ist wie folgt:

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

Zweitens Sie vorausgesetzt, Sie den einzigen Hinweis auf m_stream, halten, die nicht korrekt sind oder nicht. Das m_stream Mitglied sollte sich selbst abschließen. Aber Sie müssen nichts tun, um das zu erreichen. Letztendlich wird m_stream ein FileInputStream oder oder ein Socket-Stream sein, und sie finalisieren sich bereits richtig.

Ich würde es einfach entfernen.

+0

Ich stimme prinzipiell dem Aufruf von 'super.finalize()' zu, aber in diesem Fall erweitert 'HTTPMessage '' Object', das ein leeres 'finalize()' hat. Ich stimme zu, dass der Code in seiner jetzigen Form nicht optimal ist, aber ich bin mir nicht sicher, ob das auch relevant ist. Das Problem ist, dass 'finalize()' scheint falsch unter Java 8 aufgerufen wird, aber es war nicht unter Java 7. – Nathan

+0

Ich würde es immer noch entfernen und sehen, was passiert. Sie brauchen es nicht und es ist eine mögliche Fehlerquelle aus all den Gründen, die ich angegeben habe. – EJP

+0

Ich stimme zu, dass 'finalize()' in diesem Fall nicht benötigt wird (wir nennen 'in.close() 'direkt weiter unten im Code). Unsere HTTP/MIME-Bibliothek wird jedoch in anderen Szenarien verwendet, in denen 'finalize()' benötigt wird. Wie ich bereits erwähnt habe, werde ich die Bibliothek und all ihre Anwendungen in der Zukunft wieder besuchen, um "finalize()" zu entfernen. Ich habe auch den betreffenden Code bereits umgeschrieben, um das Problem zu vermeiden. Wir haben andere Bereiche unseres Codes, die auch 'finalize()' Methoden haben. Meine Sorge ist, dass wir in anderen Teilen unseres Codes auf dasselbe Problem stoßen könnten. – Nathan

Verwandte Themen