Um guia para o método finalize em Java

Um guia para o método finalize em Java

1. Visão geral

Neste tutorial, vamos nos concentrar em um aspecto central da linguagem Java - o métodofinalize fornecido pela classe rootObject.

Simplificando, isso é chamado antes da coleta de lixo para um objeto específico.

2. Usando finalizadores

O métodofinalize() é chamado de finalizador.

Os finalizadores são chamados quando a JVM descobre que essa instância específica deve ser coletada como lixo. Esse finalizador pode executar quaisquer operações, incluindo trazer o objeto de volta à vida.

O principal objetivo de um finalizador é, no entanto, liberar recursos usados ​​por objetos antes que eles sejam removidos da memória. Um finalizador pode funcionar como o principal mecanismo para operações de limpeza ou como uma rede de segurança quando outros métodos falham.

Para entender como funciona um finalizador, vamos dar uma olhada em uma declaração de classe:

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // other class members
}

A classeFinalizable possui um camporeader, que faz referência a um recurso que pode ser fechado. Quando um objeto é criado a partir dessa classe, ele constrói uma nova instânciaBufferedReader lendo um arquivo no caminho de classe.

Essa instância é usada no métodoreadFirstLine para extrair a primeira linha no arquivo fornecido. Notice that the reader isn’t closed in the given code.

Podemos fazer isso usando um finalizador:

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

É fácil ver que um finalizador é declarado como qualquer método de instância normal.

Na realidade,the time at which the garbage collector calls finalizers is dependent on the JVM’s implementation and the system’s conditions, which are out of our control.

Para fazer a coleta de lixo acontecer no local, vamos aproveitar as vantagens do métodoSystem.gc. Nos sistemas do mundo real, nunca devemos invocar isso explicitamente, por várias razões:

  1. É caro

  2. Não aciona a coleta de lixo imediatamente - é apenas uma dica para a JVM iniciar o GC

  3. A JVM sabe melhor quando o GC precisa ser chamado

Se precisarmos forçar GC, podemos usarjconsole para isso.

A seguir, é apresentado um caso de teste que demonstra a operação de um finalizador:

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("example.com", firstLine);
    System.gc();
}

Na primeira instrução, um objetoFinalizable é criado e, em seguida, seu métodoreadFirstLine é chamado. Este objeto não é atribuído a nenhuma variável, portanto, é elegível para coleta de lixo quando o métodoSystem.gc é chamado.

A asserção no teste verifica o conteúdo do arquivo de entrada e é usada apenas para provar que nossa classe personalizada funciona conforme o esperado.

Quando executamos o teste fornecido, uma mensagem será impressa no console sobre o leitor em buffer sendo fechado no finalizador. Isso significa que o métodofinalize foi chamado e limpou o recurso.

Até esse ponto, os finalizadores parecem uma ótima maneira de operações de pré-destruição. No entanto, isso não é bem verdade.

Na próxima seção, veremos por que usá-los deve ser evitado.

3. Evitando finalizadores

Vamos dar uma olhada em vários problemas que enfrentaremos ao usar finalizadores para realizar ações críticas.

O primeiro problema perceptível associado aos finalizadores é a falta de prontidão. Não podemos saber quando um finalizador é executado, pois a coleta de lixo pode ocorrer a qualquer momento.

Por si só, isso não é um problema porque o mais importante é que o finalizador ainda é invocado, mais cedo ou mais tarde. No entanto, os recursos do sistema são limitados. Thus, we may run out of those resources before they get a chance to be cleaned up, potentially resulting in system crashes.

Os finalizadores também têm impacto na portabilidade do programa. Como o algoritmo de coleta de lixo depende da implementação da JVM, um programa pode ser executado muito bem em um sistema enquanto se comporta de maneira diferente no tempo de execução em outro.

Outra questão importante que vem com os finalizadores é o custo de desempenho. Especificamente,JVM must perform much more operations when constructing and destroying objects containing a non-empty finalizer.

Os detalhes são específicos da implementação, mas as idéias gerais são as mesmas em todas as JVMs: etapas adicionais devem ser tomadas para garantir que os finalizadores sejam executados antes que os objetos sejam descartados. Essas etapas podem aumentar a duração da criação e destruição de objetos em centenas ou mesmo milhares de vezes.

O último problema sobre o qual falaremos é a falta de tratamento de exceções durante a finalização. If a finalizer throws an exception, the finalization process is canceled, and the exception is ignored, leaving the object in a corrupted state sem qualquer notificação.

4. Exemplo sem finalizador

Vamos explorar uma solução que fornece a mesma funcionalidade, mas sem o uso do métodofinalize(). Observe que o exemplo abaixo não é a única maneira de substituir os finalizadores.

Em vez disso, é usado para demonstrar um ponto importante: sempre há opções que nos ajudam a evitar finalizadores.

Aqui está a declaração de nossa nova classe:

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

Não é difícil ver que a única diferença entre a nova classeCloseableResource e nossa classeFinalizable anterior é a implementação da interfaceAutoCloseable em vez de uma definição de finalizador.

Observe que o corpo do métodoclose deCloseableResource é quase o mesmo que o corpo do finalizador na classeFinalizable.

A seguir, é apresentado um método de teste, que lê um arquivo de entrada e libera o recurso após o término do trabalho:

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("example.com", firstLine);
    }
}

No teste acima, uma instânciaCloseableResource é criada no blocotry de uma instrução try-with-resources, portanto, esse recurso é fechado automaticamente quando o bloco try-with-resources conclui a execução.

Executando o método de teste fornecido, veremos uma mensagem impressa do métodoclose da classeCloseableResource.

5. Conclusão

Neste tutorial, nos concentramos em um conceito central em Java - o métodofinalize. Isso parece útil no papel, mas pode ter efeitos colaterais feios em tempo de execução. E, o mais importante, sempre há uma solução alternativa para usar um finalizador.

Um ponto crítico a ser observado é quefinalize se tornou obsoleto a partir do Java 9 - e eventualmente será removido.

Como sempre, o código-fonte deste tutorial pode ser encontradoover on GitHub.