finalize () invocó un objeto fuertemente alcanzable en Java 8

Recientemente hemos actualizado nuestra aplicación de procesamiento de mensajes de Java 7 a Java 8. Desde la actualización, obtenemos una excepción ocasional de que una secuencia se ha cerrado mientras se leía. El registro muestra que el hilo del finalizador llama a finalize() en el objeto que contiene la secuencia (que a su vez cierra la secuencia).

El esquema básico del código es el siguiente:

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

MIMEWriter y MIMEBodyPart son parte de una biblioteca MIME / HTTP de cosecha propia. MIMEBodyPart extiende HTTPMessage , que tiene lo siguiente:

 public void close() throws IOException { if ( m_stream != null ) { m_stream.close(); } } protected void finalize() { try { close(); } catch ( final Exception ignored ) { } } 

La excepción se produce en la cadena de invocación de MIMEWriter.writePart , que es la siguiente:

  1. MIMEWriter.writePart() escribe los encabezados para la parte y luego llama a part.writeBodyPartContent( this )
  2. MIMEBodyPart.writeBodyPartContent() llama a nuestro método de utilidad IOUtil.copy( getContentStream(), out ) para transmitir el contenido a la salida
  3. MIMEBodyPart.getContentStream() simplemente devuelve la stream de entrada pasada al contstructor (ver el bloque de código anterior)
  4. IOUtil.copy tiene un bucle que lee un fragmento de 8K del flujo de entrada y lo escribe en el flujo de salida hasta que el flujo de entrada esté vacío.

Se MIMEBodyPart.finalize() mientras se está ejecutando IOUtil.copy , y obtiene la siguiente excepción:

 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) 

Pusimos un poco de registro en el método HTTPMessage.close() que registró el seguimiento de la stack de la persona que llama y demostró que definitivamente es la HTTPMessage.finalize() del finalizador que invoca a HTTPMessage.finalize() mientras se está ejecutando IOUtil.copy() .

El objeto MIMEBodyPart es definitivamente accesible desde la stack del hilo actual, como this en el marco de stack para MIMEBodyPart.writeBodyPartContent . No entiendo por qué la JVM llamaría a finalize() .

Traté de extraer el código relevante y ejecutarlo en un circuito cerrado en mi propia máquina, pero no puedo reproducir el problema. Podemos reproducir de manera confiable el problema con alta carga en uno de nuestros servidores de desarrollo, pero cualquier bash de crear un caso de prueba reproducible más pequeño ha fallado. El código se comstack bajo Java 7 pero se ejecuta bajo Java 8. Si volvemos a Java 7 sin volver a comstackr, el problema no ocurre.

Como solución alternativa, reescribí el código afectado utilizando la biblioteca MIME de Java Mail y el problema desapareció (presumiblemente Java Mail no utiliza finalize() ). Sin embargo, me preocupa que otros métodos finalize() en la aplicación se puedan llamar incorrectamente, o que Java esté intentando recolectar basura con objetos que todavía están en uso.

Sé que la mejor práctica actual no recomienda el uso de finalize() y probablemente vuelva a visitar esta biblioteca local para eliminar los métodos de finalize() . Dicho esto, ¿alguien se ha encontrado con este problema antes? ¿Alguien tiene alguna idea sobre la causa?

    Un poco de conjetura aquí. Es posible que un objeto se finalice y se recoja la basura incluso si hay referencias a ella en variables locales en la stack, e incluso si hay una llamada activa a un método de instancia de ese objeto en la stack. El requisito es que el objeto sea inalcanzable . Incluso si está en la stack, si ningún código posterior toca esa referencia, es potencialmente inalcanzable.

    Consulte esta otra respuesta para ver un ejemplo de cómo un objeto puede ser sometido a GC mientras que una variable local que hace referencia a él todavía está dentro del scope.

    Aquí hay un ejemplo de cómo se puede finalizar un objeto mientras está activa una llamada de método de instancia:

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

    Mientras el método loop() está activo, no hay posibilidad de que ningún código haga algo con la referencia al objeto FinalizeThis , por lo que es inalcanzable. Y por lo tanto, puede ser finalizado y GC. En JDK 8 GA, esto imprime lo siguiente:

     loop() called finalized! loop() returns 

    cada vez.

    Algo similar podría estar pasando con MimeBodyPart . ¿Está siendo almacenado en una variable local? (Parece que sí, ya que el código parece adherirse a una convención de que los campos se nombran con un prefijo m_ ).

    ACTUALIZAR

    En los comentarios, el OP sugirió realizar el siguiente cambio:

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

    Con este cambio, él no observó la finalización, y yo tampoco. Sin embargo, si se realiza este cambio adicional:

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

    finalización una vez más ocurre. Sospecho que la razón es que sin el ciclo, el método main() se interpreta, no se comstack. El intérprete probablemente sea menos agresivo con el análisis de accesibilidad. Con el ciclo de rendimiento en su lugar, el método main() se comstack y el comstackdor JIT detecta que finalizeThis se volvió inalcanzable mientras se ejecuta el método loop() .

    Otra forma de desencadenar este comportamiento es usar la opción -Xcomp en la JVM, lo que obliga a que los métodos se compilen JIT antes de su ejecución. No ejecutaría una aplicación completa de esta manera: comstackr JIT todo puede ser bastante lento y ocupar mucho espacio, pero es útil para eliminar casos como este en pequeños progtwigs de prueba, en lugar de jugar con bucles.

    Tu finalizador no es correcto.

    En primer lugar, no necesita el bloque catch, y debe llamar a super.finalize() en su propio bloque finally{} . La forma canónica de un finalizador es la siguiente:

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

    En segundo lugar, está asumiendo que tiene la única referencia a m_stream , que puede o no ser correcta. El miembro m_stream debe finalizar por sí mismo. Pero no necesitas hacer nada para lograr eso. En m_stream instancia, m_stream será un FileInputStream o FileOutputStream o un flujo de socket, y ya se finalizan correctamente.

    Simplemente lo eliminaría.