finalize() llamó a objetos fuertemente accesibles en Java 8

Resuelto Nathan asked hace 9 años • 3 respuestas

Recientemente actualizamos nuestra aplicación de procesamiento de mensajes de Java 7 a Java 8. Desde la actualización, recibimos una excepción ocasional de que una secuencia se cerró mientras se leía. El registro muestra que el subproceso finalizador está llamando finalize()al 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 );

MIMEWritery MIMEBodyPartson parte de una biblioteca MIME/HTTP propia. MIMEBodyPartextends 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 ocurre en la cadena de invocación de MIMEWriter.writePart, que es la siguiente:

  1. MIMEWriter.writePart()escribe los encabezados de la parte, luego llamapart.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 el flujo de entrada pasado al constructor (ver bloque de código arriba)
  4. IOUtil.copytiene 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()llama mientras IOUtil.copyse está ejecutando 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 algo de registro en el HTTPMessage.close()método que registró el seguimiento de la pila de la persona que llama y demostramos que definitivamente es el subproceso finalizador el que se invoca HTTPMessage.finalize()mientras IOUtil.copy()se ejecuta.

Definitivamente se puede acceder al MIMEBodyPartobjeto desde la pila del hilo actual como thisen el marco de la pila para MIMEBodyPart.writeBodyPartContent. No entiendo por qué la JVM llamaría finalize().

Intenté extraer el código relevante y ejecutarlo en un bucle cerrado en mi propia máquina, pero no puedo reproducir el problema. Podemos reproducir de manera confiable el problema con una carga alta en uno de nuestros servidores de desarrollo, pero todos los intentos de crear un caso de prueba reproducible más pequeño han fallado. El código se compila en Java 7 pero se ejecuta en Java 8. Si volvemos a Java 7 sin volver a compilar, el problema no ocurre.

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

Sé que las mejores prácticas actuales recomiendan no usarlo finalize()y probablemente volveré a visitar esta biblioteca local para eliminar los finalize()métodos. Dicho esto, ¿alguien se ha encontrado con este problema antes? ¿Alguien tiene alguna idea sobre la causa?

Nathan avatar Oct 30 '14 06:10 Nathan
Aceptado

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

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

A continuación se muestra un ejemplo de cómo se puede finalizar un objeto mientras una llamada a un método de instancia está activa:

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 loop()método está activo, no hay posibilidad de que ningún código haga nada con la referencia al FinalizeThisobjeto, por lo que es inalcanzable. Y por lo tanto se puede finalizar y editar en GC. En JDK 8 GA, esto imprime lo siguiente:

loop() called
finalized!
loop() returns

cada vez.

Algo similar podría estar pasando con MimeBodyPart. ¿Se almacena 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 m_prefijo).

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

La finalización se produce una vez más. Sospecho que la razón es que sin el bucle, el main()método se interpreta, no se compila. El intérprete probablemente sea menos agresivo con el análisis de accesibilidad. Con el ciclo de rendimiento implementado, el main()método se compila y el compilador JIT detecta que finalizeThisse ha vuelto inalcanzable mientras loop()se ejecuta el método.

Otra forma de desencadenar este comportamiento es utilizar la -Xcompopción JVM, que obliga a los métodos a compilarse JIT antes de su ejecución. No ejecutaría una aplicación completa de esta manera (la compilación JIT de todo puede ser bastante lenta y ocupar mucho espacio), pero es útil para eliminar casos como este en pequeños programas de prueba, en lugar de jugar con bucles.

Stuart Marks avatar Oct 30 '2014 05:10 Stuart Marks

finalizeTiene 99 problemas y la finalización prematura es nueva.

Java 9 ha introducido Reference.reachabilityFence para solucionar este problema. La documentación también menciona el uso synchronized (obj) { ... }como alternativa en Java 8.

Pero la verdadera solución es no consumir finalize.

Aleksandr Dubinsky avatar Oct 22 '2021 18:10 Aleksandr Dubinsky

Tu finalizador no es correcto.

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

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

En segundo lugar, asume que tiene la única referencia a m_stream, lo que puede ser correcto o no. El m_streammiembro debe finalizarse. Pero no es necesario hacer nada para lograrlo. Al final m_streamserá un flujo de socket FileInputStreamo FileOutputStreamo, y ya se finalizan correctamente.

Yo simplemente lo quitaría.

user207421 avatar Oct 30 '2014 00:10 user207421