Substituir Object.finalize () é realmente ruim?

34

Os dois principais argumentos contra a substituição Object.finalize()são:

  1. Você não decide quando é chamado.

  2. Pode não ser chamado.

Se eu entendi isso corretamente, não acho que essas sejam boas razões para odiar Object.finalize()tanto.

  1. Depende da implementação da VM e do GC determinar quando é o momento certo para desalocar um objeto, não o desenvolvedor. Por que é importante decidir quando Object.finalize()é chamado?

  2. Normalmente, e me corrija se estiver errado, o único momento em Object.finalize()que não seria chamado é quando o aplicativo era encerrado antes que o GC tivesse a chance de ser executado. No entanto, o objeto foi desalocado de qualquer maneira quando o processo do aplicativo foi encerrado com ele. Portanto Object.finalize(), não foi chamado porque não precisava ser chamado. Por que o desenvolvedor se importaria?

Toda vez que estou usando objetos que tenho que fechar manualmente (como identificadores e conexões de arquivos), fico muito frustrado. Eu tenho que verificar constantemente se um objeto possui uma implementação close()e tenho certeza de que perdi algumas chamadas para ele em alguns pontos no passado. Por que não é apenas mais simples e seguro deixar a VM e o GC descartarem esses objetos colocando a close()implementação Object.finalize()?

AxiomaticNexus
fonte
11
Observe também: como muitas APIs da era Java 1.0, a semântica de threads finalize()é um pouco complicada. Se você implementá-lo, verifique se ele é seguro para threads em relação a todos os outros métodos no mesmo objeto.
billc.cn
2
Quando você ouve pessoas dizendo que os finalizadores são ruins, isso não significa que seu programa parará de funcionar se você os tiver; eles significam que toda a idéia de finalização é bastante inútil.
user253751
11
+1 nesta pergunta. A maioria das respostas abaixo afirma que os recursos, como descritores de arquivo, são limitados e que devem ser coletados manualmente. O mesmo aconteceu / é verdadeiro com relação à memória; portanto, se aceitamos algum atraso na coleta de memória, por que não aceitá-lo para descritores de arquivos e / ou outros recursos?
Mbonnin
Dirigindo-se ao seu último parágrafo, você pode deixar para Java lidar com o fechamento de coisas como identificadores de arquivo e conexões com muito pouco esforço de sua parte. Use o bloco try-with-resources - já foi mencionado algumas vezes em respostas e comentários, mas acho que vale a pena colocar aqui. O tutorial do Oracle para isso pode ser encontrado em docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Respostas:

45

Na minha experiência, há uma e apenas uma razão para substituir Object.finalize(), mas é uma boa razão :

Para colocar o código de log de erro no finalize()qual notifica você se você esquecer de invocar close().

A análise estática pode capturar apenas omissões em cenários de uso trivial, e os avisos do compilador mencionados em outra resposta têm uma visão tão simplista das coisas que você realmente precisa desativá-las para fazer qualquer coisa não trivial. (Tenho muito mais avisos ativados do que qualquer outro programador que eu conheço ou já ouvi falar, mas não tenho avisos estúpidos ativados.)

A finalização pode parecer um bom mecanismo para garantir que os recursos não fiquem indisponíveis, mas a maioria das pessoas vê isso de uma maneira completamente errada: eles pensam nisso como um mecanismo alternativo de fallback, uma "segunda chance" de salvaguarda que salvará automaticamente o dia, descartando os recursos que eles esqueceram. Isso está completamente errado . Deve haver apenas uma maneira de fazer qualquer coisa: você sempre fecha tudo ou a finalização sempre fecha tudo. Mas como a finalização não é confiável, a finalização não pode ser isso.

Portanto, existe esse esquema que chamo de Disposição Obrigatória , e estipula que o programador é responsável por sempre fechar explicitamente tudo o que implementa Closeableou AutoCloseable. (A instrução try-with-resources ainda conta como fechamento explícito.) É claro que o programador pode esquecer, então é aí que a finalização entra em jogo, mas não como uma fada mágica que, magicamente, fará as coisas corretamente no final: se a finalização descobrir que close()não foi invocado, nãotente invocá-lo, precisamente porque (com certeza matemática) haverá hordas de programadores do n00b que confiarão nele para fazer o trabalho que eles eram preguiçosos ou distraídos demais para fazer. Portanto, com o descarte obrigatório, quando a finalização descobre que close()não foi invocada, ela registra uma mensagem de erro em vermelho brilhante, dizendo ao programador com letras maiúsculas grandes e grandes para consertar suas coisas.

Como um benefício adicional, dizem os boatos de que "a JVM ignorará um método trivial finalize () (por exemplo, um que apenas retorna sem fazer nada, como o definido na classe Object)"; portanto, com o descarte obrigatório, você pode evitar toda finalização sobrecarga em todo o sistema ( consulte a resposta da alip para obter informações sobre quão terrível é essa sobrecarga) codificando seu finalize()método da seguinte maneira:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

A idéia por trás disso é que Global.DEBUGé uma static finalvariável cujo valor é conhecido no momento da compilação; portanto, se for false, o compilador não emitirá nenhum código para toda a ifinstrução, o que tornará este um finalizador trivial (vazio), que por sua vez significa que sua turma será tratada como se não tivesse um finalizador. (Em C #, isso seria feito com um bom #if DEBUGbloco, mas o que podemos fazer, isso é java, onde pagamos aparente simplicidade no código com sobrecarga adicional no cérebro.)

Mais sobre o descarte obrigatório, com discussões adicionais sobre o descarte de recursos no dot Net, aqui: michael.gr: descarte obrigatório versus a abominação "Dispose-descarte"

Mike Nakis
fonte
2
@MikeNakis Não se esqueça, Closeable é definido como não fazer nada se for chamado uma segunda vez: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Eu admito que às vezes registrei um aviso quando minhas aulas são fechadas duas vezes, mas tecnicamente você nem deveria fazer isso. Tecnicamente, porém, chamar .close () em Closable mais de uma vez é perfeitamente válido.
Patrick M
11
@usr tudo se resume a você confiar nos seus testes ou nos seus testes. Se você não confiar nos seus testes, com certeza, vá em frente e sofra a sobrecarga de finalização também close() , por precaução. Acredito que, se meus testes não são confiáveis, é melhor não liberar o sistema para produção.
5605 Mike Nakis
3
@ Mike, para que a if( Global.DEBUG && ...construção funcione, de modo que a JVM ignore o finalize()método como trivial, Global.DEBUGdeve ser configurada no tempo de compilação (em oposição a injetado etc.), para que o que se segue seja código morto. A chamada para super.finalize()fora do bloco if também é suficiente para a JVM tratá-lo como não trivial (independentemente do valor de Global.DEBUG, pelo menos no HotSpot 1.8), mesmo que a superclasse #finalize()seja trivial!
Alip
11
@ Mike, receio que seja esse o caso. Testei-o com (uma versão ligeiramente modificada) do teste no artigo ao qual você vinculou e a saída detalhada do GC (juntamente com um desempenho surpreendentemente ruim) confirma que os objetos são copiados para o espaço sobrevivente / da geração antiga e precisam do GC de pilha completo para obter livrar de.
Alip
11
O que geralmente é esquecido é o risco de uma coleção de objetos anterior ao esperado, que torna a liberação de recursos no finalizador uma ação muito perigosa. Antes do Java 9, a única maneira de garantir que um finalizador não feche o recurso enquanto ele ainda está em uso, é sincronizar no objeto, no finalizador e nos métodos que usam o recurso. É por isso que funciona java.io. Se esse tipo de segurança de thread não estava na lista de desejos, isso aumenta a sobrecarga induzida por finalize()...
Holger
28

Toda vez que estou usando objetos que tenho que fechar manualmente (como identificadores e conexões de arquivos), fico muito frustrado. [...] Por que não é apenas mais simples e seguro deixar a VM e o GC descartarem esses objetos colocando a close()implementação Object.finalize()?

Como identificadores e conexões de arquivos (ou seja, descritores de arquivos nos sistemas Linux e POSIX) são um recurso bastante escasso (você pode estar limitado a 256 deles em alguns sistemas ou a 16384 em outros; veja setrlimit (2) ). Não há garantia de que o GC seja chamado com bastante frequência (ou no momento certo) para evitar o esgotamento de tais recursos limitados. E se o GC não for chamado o suficiente (ou a finalização não for executada no momento certo), você atingirá esse limite (possivelmente baixo).

A finalização é uma coisa de "melhor esforço" na JVM. Pode não ser chamado ou tarde demais ... Em particular, se você tem muita RAM ou se o seu programa não aloca muitos objetos (ou se a maioria deles morre antes de ser encaminhada para alguém com idade suficiente) geração por um GC geracional de cópia), o GC pode ser chamado muito raramente, e as finalizações podem não ser executadas com muita frequência (ou até mesmo não serem executadas).

Portanto, descreva os closearquivos explicitamente, se possível. Se você tem medo de vazá-los, use a finalização como uma medida extra, não como a principal.

Basile Starynkevitch
fonte
7
Eu acrescentaria que o fechamento de um fluxo em um arquivo ou soquete normalmente o libera. Deixando fluxos abrir desnecessariamente aumenta o risco de perda de dados se o poder sai, a conexão cai (este também é um risco para arquivos acessados através de uma rede), etc.
2
Na verdade, enquanto o número de filedescriptors permitidos pode ser baixa, isso não é o mesmo ponto pegajoso, porque um poderia usar isso como um sinal para a GC, pelo menos. A coisa realmente problemática é: a) completamente intransparente para o GC quantos recursos não gerenciados pelo GC dependem dele eb) muitos desses recursos são únicos, portanto outros poderão ser bloqueados ou negados.
Deduplicator
2
E se você deixar um arquivo aberto, ele poderá interferir na utilização de outra pessoa. (Visualizador do Windows 8 XPS, estou olhando para você!)
Loren Pechtel
2
"Se você tem medo de vazar [descritores de arquivo], use a finalização como uma medida extra, não como a principal." Esta afirmação parece suspeita para mim. Se você projetou bem seu código, deveria introduzir um código redundante que espalhe a limpeza em vários locais?
Mucaho 5/07
2
@BasileStarynkevitch Seu argumento é que, em um mundo ideal, sim, a redundância é ruim, mas na prática, onde você não pode prever todos os aspectos relevantes, é melhor prevenir do que remediar?
Mucaho 06/07/2015
13

Veja o problema desta maneira: você só deve escrever um código que seja (a) correto (caso contrário, seu programa está completamente errado) e (b) necessário (caso contrário, seu código é muito grande, o que significa mais RAM necessária, mais ciclos gastos em coisas inúteis, mais esforço para entendê-lo, mais tempo gasto em mantê-lo etc. etc.)

Agora considere o que você deseja fazer em um finalizador. Ou é necessário. Nesse caso, você não pode colocá-lo em um finalizador, porque você não sabe se ele será chamado. Isso não é bom o suficiente. Ou não é necessário - então você não deveria escrevê-lo em primeiro lugar! De qualquer forma, colocá-lo no finalizador é a escolha errada.

(Observe que os exemplos que você nomeia, como fechar fluxos de arquivos, parecem que não são realmente necessários, mas são. É apenas que, até atingir o limite de identificadores de arquivos abertos em seu sistema, você não notará que seus O código está incorreto. Mas esse limite é um recurso do sistema operacional e, portanto, é ainda mais imprevisível do que a política da JVM para finalizadores; portanto, é realmente importante que você não desperdice identificadores de arquivo.)

Kilian Foth
fonte
Se eu devo escrever apenas o código "necessário", devo evitar todo o estilo em todos os meus aplicativos GUI? Certamente nada disso é necessário; as GUIs funcionarão bem sem estilo, parecerão horríveis. Sobre o finalizador, pode ser necessário fazer algo, mas ainda assim, é possível colocá-lo em um finalizador, pois o finalizador será chamado durante o objeto GC, se houver. Se você precisar fechar um recurso, especificamente quando estiver pronto para a coleta de lixo, um finalizador é ideal. O programa termina de liberar seu recurso ou o finalizador é chamado.
Kröw 20/06
Se eu tiver um objeto que possa ser pesado, caro para criar e feche a RAM e decido referenciá-lo de maneira fraca, para que possa ser limpo quando não for necessário, eu poderia usá finalize()-lo para fechar caso um ciclo gc realmente precise liberar RAM. Caso contrário, eu manteria o objeto na RAM, em vez de precisar regenerá-lo e fechá-lo toda vez que precisar usá-lo. Claro, os recursos que ele abre não serão liberados até que o objeto seja analisado em GC, sempre que possível, mas talvez não seja necessário garantir que meus recursos sejam liberados em um determinado momento.
Kröw 20/06
8

Uma das maiores razões para não confiar nos finalizadores é que a maioria dos recursos que se pode tentar limpar no finalizador são muito limitados. O coletor de lixo é executado apenas de vez em quando, pois percorre referências para determinar se algo pode ser liberado ou não é caro. Isso significa que pode demorar um pouco até que seus objetos sejam destruídos. Se você tiver muitos objetos abrindo conexões de banco de dados de curta duração, por exemplo, deixar os finalizadores para limpar essas conexões pode esgotar seu conjunto de conexões enquanto aguarda o coletor de lixo finalmente executar e liberar as conexões concluídas. Então, devido à espera, você acaba com um grande estoque de solicitações enfileiradas, que esgotam rapidamente o conjunto de conexões novamente. Isto'

Além disso, o uso do try-with-resources facilita o fechamento de objetos 'fechados' na conclusão. Se você não estiver familiarizado com essa construção, sugiro que verifique: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Cães
fonte
8

Além do motivo pelo qual deixar para o finalizador liberar recursos geralmente é uma má idéia, os objetos finalizáveis ​​vêm com sobrecarga de desempenho.

Da teoria e prática Java: Coleta de lixo e desempenho (Brian Goetz), os finalizadores não são seus amigos :

Objetos com finalizadores (aqueles que possuem um método finalize () não trivial) têm uma sobrecarga significativa em comparação com objetos sem finalizadores e devem ser usados ​​com moderação. Objetos finalizáveis ​​são mais lentos para alocar e mais lentos para coletar. No momento da alocação, a JVM deve registrar qualquer objeto finalizável no coletor de lixo e (pelo menos na implementação da JVM HotSpot) os objetos finalizáveis ​​devem seguir um caminho de alocação mais lento que a maioria dos outros objetos. Da mesma forma, os objetos finalizáveis ​​também são mais lentos para coletar. São necessários pelo menos dois ciclos de coleta de lixo (na melhor das hipóteses) antes que um objeto finalizável possa ser recuperado, e o coletor de lixo precisa fazer um trabalho extra para chamar o finalizador. O resultado é mais tempo gasto alocando e coletando objetos e mais pressão no coletor de lixo, porque a memória usada por objetos finalizáveis ​​inacessíveis é mantida por mais tempo. Combine isso com o fato de que os finalizadores não são garantidos para execução em qualquer período de tempo previsível, ou mesmo em todos, e você pode ver que existem relativamente poucas situações para as quais a finalização é a ferramenta certa a ser usada.

alip
fonte
Ponto excelente. Veja minha resposta, que fornece um meio de evitar a sobrecarga de desempenho finalize().
9788 Mike Nakis
7

Meu (menos) motivo favorito para evitar Object.finalizenão é que os objetos possam ser finalizados depois que você espera, mas eles podem ser finalizados antes que você possa esperar. O problema não é que um objeto que ainda esteja no escopo possa ser finalizado antes que o escopo seja encerrado, se Java decidir que não é mais acessível.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

Veja esta pergunta para mais detalhes. Ainda mais divertido é que essa decisão só pode ser tomada após a otimização do hot-spot, tornando difícil a depuração.

Michael Anderson
fonte
11
A questão é que HasFinalize.streamele próprio deve ser um objeto finalizável separadamente. Ou seja, a finalização de HasFinalizenão deve finalizar ou tentar limpar stream. Ou, se deveria, deveria ficar streaminacessível.
acelent
11
É ainda pior
Holger
4

Eu tenho que estar constantemente verificando se um objeto tem uma implementação de close (), e tenho certeza que perdi algumas chamadas para ele em alguns pontos no passado.

No Eclipse, recebo um aviso sempre que esqueço de fechar algo que implementa Closeable/ AutoCloseable. Não tenho certeza se isso é algo do Eclipse ou se faz parte do compilador oficial, mas você pode usar ferramentas de análise estática semelhantes para ajudá-lo lá. O FindBugs, por exemplo, provavelmente pode ajudá-lo a verificar se você esqueceu de fechar um recurso.

Nebu Pookins
fonte
11
Boa ideia mencionando AutoCloseable. Isso facilita o gerenciamento de recursos com tentativa com recursos. Isso anula vários dos argumentos da pergunta.
2

Para sua primeira pergunta:

Depende da implementação da VM e do GC determinar quando é o momento certo para desalocar um objeto, não o desenvolvedor. Por que é importante decidir quando Object.finalize()é chamado?

Bem, a JVM determinará quando é um bom ponto recuperar o armazenamento que foi alocado para um objeto. Esse não é necessariamente o momento em que o recurso de limpeza, no qual você deseja executar finalize(), deve ocorrer. Isso é ilustrado na pergunta “finalize () chamado objeto altamente alcançável em Java 8” no SO. Lá, um close()método foi chamado por um finalize()método, enquanto uma tentativa de ler do fluxo pelo mesmo objeto ainda está pendente. Portanto, além da possibilidade bem conhecida, finalize()chamada tarde demais, existe a possibilidade de ser chamada cedo demais.

A premissa de sua segunda pergunta:

Normalmente, e me corrija se estiver errado, o único momento em Object.finalize()que não seria chamado é quando o aplicativo era encerrado antes que o GC tivesse a chance de ser executado.

está simplesmente errado. Não há nenhum requisito para uma JVM suportar a finalização. Bem, não está completamente errado, pois você ainda pode interpretá-lo como "o aplicativo foi encerrado antes da finalização", supondo que o seu aplicativo seja encerrado.

Mas observe a pequena diferença entre "GC" da sua declaração original e o termo "finalização". A coleta de lixo é distinta da finalização. Uma vez que o gerenciamento de memória detecte que um objeto está inacessível, ele pode simplesmente recuperar seu espaço, caso contrário, não possui um finalize()método especial ou a finalização é simplesmente não suportada ou pode enfileirar o objeto para finalização. Portanto, a conclusão de um ciclo de coleta de lixo não implica que os finalizadores sejam executados. Isso pode acontecer posteriormente, quando a fila é processada, ou nunca.

Esse ponto também é o motivo pelo qual, mesmo em JVMs com suporte à finalização, confiar nele para a limpeza de recursos é perigoso. A coleta de lixo faz parte do gerenciamento de memória e, portanto, é acionada pelas necessidades de memória. É possível que a coleta de lixo nunca seja executada porque há memória suficiente durante todo o tempo de execução (bem, isso ainda se encaixa na descrição “o aplicativo foi encerrado antes que o GC tivesse a chance de executar”), de alguma forma). Também é possível que o GC seja executado, mas, posteriormente, há memória recuperada suficiente, para que a fila do finalizador não seja processada.

Em outras palavras, os recursos nativos gerenciados dessa maneira ainda são estranhos ao gerenciamento de memória. Embora seja garantido que um OutOfMemoryErroré lançado somente após tentativas suficientes para liberar memória, isso não se aplica aos recursos e finalização nativos. É possível que a abertura de um arquivo falhe devido a recursos insuficientes, enquanto a fila de finalização estiver cheia de objetos que poderiam liberar esses recursos, se o finalizador alguma vez…

Holger
fonte