Procurando informações sobre decisões sobre o design da linguagem coletada pelo lixo. Talvez um especialista em idiomas possa me esclarecer? Eu venho de um background em C ++, então essa área é desconcertante para mim.
Parece que quase todas as linguagens modernas de coleta de lixo com suporte a objetos OOPy, como Ruby, Javascript / ES6 / ES7, Actionscript, Lua etc., omitem completamente o paradigma destruidor / finalizado. Python parece ser o único com seu class __del__()
método. Por que é isso? Existem limitações funcionais / teóricas em idiomas com coleta automática de lixo que impedem implementações efetivas de um método destruidor / finalizador em objetos?
Acho extremamente carente que essas linguagens considerem a memória como o único recurso que vale a pena gerenciar. E quanto a soquetes, identificadores de arquivo, estados de aplicativos? Sem a capacidade de implementar lógica personalizada para limpar recursos e estados que não sejam de memória na finalização de objetos, sou obrigado a agrupar meu aplicativo com myObject.destroy()
chamadas de estilo personalizado , colocando a lógica de limpeza fora da minha "classe", interrompendo a tentativa de encapsulamento e relegando meu aplicativo para vazamentos de recursos devido a erro humano, em vez de ser tratado automaticamente pelo gc.
Quais são as decisões de design de linguagem que levam a essas linguagens não ter nenhuma maneira de executar lógica personalizada no descarte de objetos? Eu tenho que imaginar que há uma boa razão. Eu gostaria de entender melhor as decisões técnicas e teóricas que resultaram nessas linguagens sem suporte para destruição / finalização de objetos.
Atualizar:
Talvez seja uma maneira melhor de expressar minha pergunta:
Por que uma linguagem teria o conceito interno de instâncias de objetos com estruturas de classe ou de classe, juntamente com instanciação personalizada (construtores), mas omitir completamente a funcionalidade de destruição / finalização? Os idiomas que oferecem coleta automática de lixo parecem ser os principais candidatos para apoiar a destruição / finalização de objetos, pois sabem com 100% de certeza quando um objeto não está mais em uso. No entanto, a maioria desses idiomas não o suporta.
Eu não acho que seja um caso em que o destruidor nunca seja chamado, pois isso seria um vazamento de memória principal, que os gcs foram projetados para evitar. Pude ver um possível argumento: o destruidor / finalizador pode não ser chamado até um tempo indeterminado no futuro, mas isso não impediu o Java ou o Python de suportar a funcionalidade.
Quais são os principais motivos de design de linguagem para não oferecer suporte a nenhuma forma de finalização de objeto?
finalize
/destroy
é uma mentira? Não há garantia de que algum dia será executado. E, mesmo que você não saiba quando (dada a coleta automática de lixo), e se o contexto necessário ainda estiver lá (ele já pode ter sido coletado). Portanto, é mais seguro garantir um estado consistente de outras maneiras, e é possível forçar o programador a fazê-lo.Respostas:
O padrão do qual você está falando, em que os objetos sabem como limpar seus recursos, se enquadra em três categorias relevantes. Não vamos confundir destruidores com finalizadores - apenas um está relacionado à coleta de lixo:
O padrão finalizador : método de limpeza declarado automaticamente, definido pelo programador, chamado automaticamente.
Os finalizadores são chamados automaticamente antes da desalocação por um coletor de lixo. O termo se aplica se o algoritmo de coleta de lixo empregado puder determinar os ciclos de vida do objeto.
O método destructor pattern : cleanup declarado automaticamente, definido pelo programador, chamado automaticamente apenas algumas vezes.
Os destruidores podem ser chamados automaticamente para objetos alocados à pilha (porque a vida útil do objeto é determinística), mas devem ser chamados explicitamente em todos os caminhos de execução possíveis para objetos alocados em heap (porque a vida útil do objeto não é determinística).
O padrão de descarte : método de limpeza declarado, definido e chamado pelo programador.
Os programadores fazem um método de descarte e chamam a si mesmos - é aqui que o seu
myObject.destroy()
método personalizado se encaixa. Se o descarte for absolutamente necessário, os descartadores devem ser chamados em todos os caminhos de execução possíveis.Os finalizadores são os dróides que você está procurando.
O padrão do finalizador (o padrão que sua pergunta está perguntando) é o mecanismo para associar objetos aos recursos do sistema (soquetes, descritores de arquivo etc.) para recuperação mútua por um coletor de lixo. Porém, os finalizadores estão fundamentalmente à mercê do algoritmo de coleta de lixo em uso.
Considere esta sua suposição:
Tecnicamente falso (obrigado, @babou). A coleta de lixo é fundamentalmente sobre memória, não objetos. Se ou quando um algoritmo de coleção percebe que a memória de um objeto não está mais em uso, depende do algoritmo e (possivelmente) de como seus objetos se referem um ao outro. Vamos falar sobre dois tipos de coletores de lixo em tempo de execução. Existem várias maneiras de alterar e aumentar essas técnicas básicas:
Rastreamento GC. Esses rastreiam a memória, não os objetos. A menos que aumentado para isso, eles não mantêm referências anteriores a objetos da memória. A menos que aumentados, esses GCs não saberão quando um objeto pode ser finalizado, mesmo que saibam quando sua memória está inacessível. Portanto, as chamadas para finalizador não são garantidas.
Contagem de referência GC . Eles usam objetos para rastrear a memória. Eles modelam a acessibilidade de objetos com um gráfico direcionado de referências. Se houver um ciclo no seu gráfico de referência de objetos, todos os objetos do ciclo nunca terão seu finalizador chamado (até o término do programa, obviamente). Novamente, as chamadas do finalizador não são garantidas.
TLDR
A coleta de lixo é difícil e diversificada. Uma chamada de finalizador não pode ser garantida antes do encerramento do programa.
fonte
finalize()
fizer com que o objeto que está sendo limpo seja referenciado novamente?). No entanto, não foi possível garantir a chamada do finalizador antes da finalização do programa não impediu o Java de suportá-lo. Não dizendo que sua resposta está incorreta, apenas incompleta. Ainda é um post muito bom. Obrigado.Em poucas palavras
A finalização não é uma questão simples de ser tratada pelos coletores de lixo. É fácil de usar com o GC de contagem de referência, mas essa família de GC geralmente é incompleta, exigindo que as perdas de memória sejam compensadas pelo acionamento explícito da destruição e finalização de alguns objetos e estruturas. O rastreamento de coletores de lixo é muito mais eficaz, mas torna muito mais difícil identificar o objeto a ser finalizado e destruído, em vez de apenas identificar a memória não utilizada, exigindo, portanto, um gerenciamento mais complexo, com um custo no tempo e no espaço e na complexidade de a implementação.
Introdução
Suponho que o que você está perguntando é por que as linguagens de coleta de lixo não tratam automaticamente a destruição / finalização no processo de coleta de lixo, conforme indicado pela observação:
Não concordo com a resposta aceita dada por kdbanman . Embora os fatos declarados sejam na maioria corretos, embora fortemente tendenciosos à contagem de referências, não creio que eles expliquem adequadamente a situação reclamada na pergunta.
Não acredito que a terminologia desenvolvida nessa resposta seja um problema, e é mais provável que confunda as coisas. De fato, como apresentado, a terminologia é determinada principalmente pela maneira como os procedimentos são ativados, e não pelo que eles fazem. O ponto é que, em todos os casos, há a necessidade de finalizar um objeto que não é mais necessário com algum processo de limpeza e liberar quaisquer recursos que esteja utilizando, sendo a memória apenas um deles. Idealmente, tudo isso deve ser feito automaticamente quando o objeto não for mais utilizado, por meio de um coletor de lixo. Na prática, o GC pode estar ausente ou apresentar deficiências, e isso é compensado pelo disparo explícito pelo programa de finalização e recuperação.
A exploração explícita pelo programa é um problema, pois pode permitir erros de programação difíceis de analisar, quando um objeto ainda em uso está sendo explicitamente finalizado.
Portanto, é muito melhor contar com a coleta automática de lixo para recuperar recursos. Mas há dois problemas:
alguma técnica de coleta de lixo permitirá vazamentos de memória que impedem a recuperação completa de recursos. Isso é bem conhecido pela referência na contagem de GC, mas pode aparecer para outras técnicas de GC ao usar algumas organizações de dados sem cuidados (ponto não discutido aqui).
embora a técnica de GC possa ser boa para identificar recursos de memória que não são mais usados, a finalização de objetos contidos nela pode não ser simples e isso complica o problema de recuperar outros recursos usados por esses objetos, que geralmente é o objetivo da finalização.
Finalmente, um ponto importante frequentemente esquecido é que os ciclos de GC podem ser acionados por qualquer coisa, não apenas pela falta de memória, se os ganchos adequados forem fornecidos e se o custo de um ciclo de GC for considerado valioso. Portanto, não há problema em iniciar um GC quando estiver faltando algum tipo de recurso, na esperança de liberar alguns.
Referência contando coletores de lixo
A contagem de referência é uma técnica fraca de coleta de lixo , que não manipula os ciclos adequadamente. Seria, de fato, fraco na destruição de estruturas obsoletas e na recuperação de outros recursos simplesmente porque é fraco na recuperação de memória. Porém, os finalizadores podem ser usados com mais facilidade com um coletor de lixo de contagem de referência (GC), pois um GC de ref-count recupera uma estrutura quando seu número de ref desce para 0, quando seu endereço é conhecido juntamente com seu tipo, estaticamente ou dinamicamente. Portanto, é possível recuperar a memória precisamente após a aplicação do finalizador adequado e a chamada recursiva do processo em todos os objetos apontados (possivelmente através do procedimento de finalização).
Em poucas palavras, a finalização é fácil de implementar com o GC de contagem de referência, mas sofre com a "incompletude" desse GC, devido a estruturas circulares, exatamente na mesma extensão que a recuperação de memória sofre. Em outras palavras, com contagem de referência, a memória é precisamente tão mal gerenciada quanto outros recursos , como soquetes, identificadores de arquivo, etc.
De fato, a incapacidade do GC de ref count para recuperar estruturas em loop (em geral) pode ser vista como vazamento de memória . Você não pode esperar que todo o GC evite vazamentos de memória. Depende do algoritmo do GC e das informações da estrutura de tipos disponíveis dinamicamente (por exemplo, no GC conservador ).
Rastreando coletores de lixo
A família mais poderosa do GC, sem esses vazamentos, é a família de rastreamento que explora as partes vivas da memória, começando por indicadores de raiz bem identificados. Todas as partes da memória que não são visitadas nesse processo de rastreamento (que podem realmente ser decompostas de várias maneiras, mas preciso simplificar) são partes não utilizadas da memória que podem ser recuperadas 1 . Esses coletores recuperam todas as partes da memória que não podem mais ser acessadas pelo programa, não importa o que ele faça. Ele recupera estruturas circulares, e o GC mais avançado é baseado em alguma variação desse paradigma, às vezes altamente sofisticado. Pode ser combinado com a contagem de referência em alguns casos e compensar suas fraquezas.
Um problema é que a sua declaração (no final da pergunta):
é tecnicamente incorreto para rastrear coletores.
O que se sabe com 100% de certeza é que partes da memória não estão mais em uso . (Mais precisamente, deve-se dizer que eles não estão mais acessíveis , porque algumas partes, que não podem mais ser usadas de acordo com a lógica do programa, ainda são consideradas em uso se ainda houver um ponteiro inútil para elas no programa .) Mas são necessários processamento adicional e estruturas apropriadas para saber quais objetos não utilizados podem ter sido armazenados nessas partes da memória que agora não são usadas . Isso não pode ser determinado pelo que é conhecido do programa, pois o programa não está mais conectado a essas partes da memória.
Assim, após um passo na coleta de lixo, você fica com fragmentos de memória que contêm objetos que não estão mais em uso, mas não há como, a priori, saber o que são esses objetos para aplicar a finalização correta. Além disso, se o coletor de rastreamento for do tipo de marcação e varredura, pode ser que alguns dos fragmentos possam conter objetos que já foram finalizados em uma passagem anterior do GC, mas não foram utilizados desde então por motivos de fragmentação. No entanto, isso pode ser resolvido usando a digitação explícita estendida.
Embora um coletor simples recupere esses fragmentos de memória, sem mais delongas, a finalização exige uma passagem específica para explorar essa memória não utilizada, identificar os objetos ali contidos e aplicar procedimentos de finalização. Mas tal exploração requer determinação do tipo de objetos que foram armazenados lá e também é necessária a determinação do tipo para aplicar a finalização adequada, se houver.
Isso implica custos extras no tempo do GC (a passagem extra) e possivelmente custos extras de memória para disponibilizar informações de tipo adequadas durante essa passagem por diversas técnicas. Esses custos podem ser significativos, pois muitas vezes é necessário finalizar apenas alguns objetos, enquanto a sobrecarga de tempo e espaço pode envolver todos os objetos.
Outro ponto é que a sobrecarga de tempo e espaço pode estar relacionada à execução do código do programa, e não apenas à execução do GC.
Não posso dar uma resposta mais precisa, apontando para questões específicas, porque não conheço as especificidades de muitos dos idiomas listados. No caso de C, a digitação é uma questão muito difícil que leva ao desenvolvimento de colecionadores conservadores. Meu palpite seria que isso também afeta C ++, mas eu não sou especialista em C ++. Isso parece ser confirmado por Hans Boehm, que fez grande parte da pesquisa sobre GC conservador. O GC conservador não pode recuperar sistematicamente toda a memória não utilizada precisamente porque pode não ter informações precisas sobre o tipo de dados. Pelo mesmo motivo, não seria possível aplicar sistematicamente os procedimentos de finalização.
Portanto, é possível fazer o que você está perguntando, como você sabe em alguns idiomas. Mas não vem de graça. Dependendo do idioma e de sua implementação, isso pode acarretar um custo, mesmo quando você não usa o recurso. Várias técnicas e trade-offs podem ser consideradas para resolver esses problemas, mas isso está além do escopo de uma resposta de tamanho razoável.
1 - esta é uma apresentação abstrata da coleção de rastreio (abrangendo o GC de copiar e marcar e varrer), as coisas variam de acordo com o tipo de coletor de rastreio, e explorar a parte não utilizada da memória é diferente, dependendo de copiar ou marcar e varredura é usada.
fonte
getting memory recycled
, que eu chamoreclamation
, e faça alguma limpeza antes disso, como recuperar outros recursos ou atualizar algumas tabelas de objetos, que eu chamofinalization
. Essas questões me pareciam relevantes, mas posso ter perdido um ponto em sua terminologia que era novo para mim.O padrão destruidor de objetos é fundamental para o tratamento de erros na programação de sistemas, mas não tem nada a ver com a coleta de lixo. Em vez disso, tem a ver com corresponder a vida útil do objeto a um escopo e pode ser implementado / usado em qualquer idioma que possua funções de primeira classe.
Exemplo (pseudocódigo). Suponha que você tenha um tipo de "arquivo bruto", como o tipo de descritor de arquivo Posix. Há quatro operações fundamentais,
open()
,close()
,read()
,write()
. Você gostaria de implementar um tipo de arquivo "seguro" que sempre limpa depois de si próprio. (Ou seja, que possui um construtor e destruidor automático.)Assumirei que nosso idioma tem tratamento de exceção com
throw
,try
efinally
(em idiomas sem tratamento de exceção, você pode configurar uma disciplina em que o usuário do seu tipo retorna um valor especial para indicar um erro.)Você configura uma função que aceita uma função que faz o trabalho. A função worker aceita um argumento (um identificador para o arquivo "seguro").
Você também fornece implementações de
read()
ewrite()
parasafe_file
(que apenas chamamraw_file
read()
ewrite()
). Agora o usuário usa osafe_file
tipo assim:Um destruidor de C ++ é realmente apenas açúcar sintático para um
try-finally
bloco. Praticamente tudo o que fiz aqui é converter o que umasafe_file
classe C ++ com um construtor e destruidor compilaria. Observe que o C ++ não possuifinally
suas exceções, especificamente porque Stroustrup achava que o uso de um destruidor explícito era melhor sintaticamente (e ele o introduziu no idioma antes que o idioma tivesse funções anônimas).(Essa é uma simplificação de uma das maneiras pelas quais as pessoas lidam com erros em linguagens do tipo Lisp há muitos anos. Acho que a encontrei no final dos anos 80 ou início dos anos 90, mas não me lembro onde.)
fonte
safe_file
ewith_file_opened_for_read
(um objeto que se fecha quando sai do escopo ) O importante é que não tem a mesma sintaxe que os construtores é irrelevante. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript, Clojure suportam funções suficientes de primeira classe, portanto, não precisam de destruidores para fornecer o mesmo recurso útil.Essa não é uma resposta completa para a pergunta, mas eu gostaria de adicionar algumas observações que não foram abordadas nas outras respostas ou comentários.
A pergunta supõe implicitamente que estamos falando de uma linguagem orientada a objetos no estilo Simula, que é ela mesma limitadora. Na maioria dos idiomas, mesmo aqueles com objetos, nem tudo é um objeto. O mecanismo para implementar destruidores imporia um custo que nem todo implementador de linguagem está disposto a pagar.
O C ++ tem algumas garantias implícitas sobre a ordem de destruição. Se você tiver uma estrutura de dados semelhante a uma árvore, por exemplo, os filhos serão destruídos antes do pai. Esse não é o caso nas linguagens do GC, portanto, os recursos hierárquicos podem ser liberados em uma ordem imprevisível. Para recursos que não sejam de memória, isso pode importar.
fonte
Quando as duas estruturas de GC mais populares (Java e .NET) estavam sendo projetadas, acho que os autores esperavam que a finalização funcionasse bem o suficiente para evitar a necessidade de outras formas de gerenciamento de recursos. Muitos aspectos do design de linguagem e estrutura podem ser bastante simplificados se não houver necessidade de todos os recursos necessários para acomodar um gerenciamento de recursos 100% confiável e determinístico. No C ++, é necessário distinguir entre os conceitos de:
Ponteiro / referência que identifica um objeto que pertence exclusivamente ao detentor da referência e que não é identificado por nenhum ponteiro / referência que o proprietário não conheça.
Ponteiro / referência que identifica um objeto compartilhável que não pertence exclusivamente a ninguém.
Ponteiro / referência que identifica um objeto que pertence exclusivamente ao detentor da referência, mas ao qual pode ser acessado através de "visualizações", o proprietário não tem como rastrear.
Ponteiro / referência que identifica um objeto que fornece uma visão de um objeto que pertence a outra pessoa.
Se uma linguagem / estrutura do GC não precisar se preocupar com o gerenciamento de recursos, todas as opções acima podem ser substituídas por um único tipo de referência.
Eu achava ingênua a ideia de que a finalização eliminaria a necessidade de outras formas de gerenciamento de recursos, mas, se essa expectativa era razoável ou não na época, a história mostrou desde então que existem muitos casos que exigem gerenciamento de recursos mais preciso do que a finalização fornece. . Por acaso, acho que as recompensas de reconhecer a propriedade no nível da linguagem / estrutura seriam suficientes para justificar o custo (a complexidade precisa existir em algum lugar, e movê-la para a linguagem / estrutura simplificaria o código do usuário), mas reconheço que existem diferenças significativas. os benefícios de design de ter um único "tipo" de referência - algo que só funciona se a linguagem / estrutura não for independente de problemas de limpeza de recursos.
fonte
O destruidor em C ++ realmente faz duas coisas combinadas. Ele libera RAM e identificações de recursos.
Outros idiomas separam essas preocupações, fazendo com que o GC seja responsável por liberar RAM, enquanto outro recurso de idioma se encarrega de liberar IDs de recursos.
É disso que se trata o GC. Eles só possuem uma coisa e é garantir que você não fique sem memória. Se a RAM for infinita, todos os GCs serão aposentados, pois não há mais motivo real para sua existência.
Os idiomas podem fornecer diferentes maneiras de liberar os IDs de recursos:
manual
.CloseOrDispose()
espalhado pelo códigomanual
.CloseOrDispose()
espalhado dentro do "finally
bloco" manualmanuais "blocos de ID de recurso" (ou seja
using
,with
,try
-com-recursos , etc.) que automatiza.CloseOrDispose()
após o bloco é feitogarantidos "blocos de ID de recurso" que automatiza
.CloseOrDispose()
após o bloco é feitoMuitos idiomas usam mecanismos manuais (e não garantidos), o que cria uma oportunidade para a má administração de recursos. Pegue este código simples do NodeJS:
..onde o programador esqueceu de fechar o arquivo aberto.
Enquanto o programa continuar em execução, o arquivo aberto ficará preso no limbo. Isso é fácil de verificar, tentando abrir o arquivo usando o HxD e verificando se não pode ser feito:
A liberação de identificações de recursos nos destruidores de C ++ também não é garantida. Você pode pensar que o RAII opera como "blocos de identificação de recurso" garantidos, mas, diferentemente de "blocos de identificação de recurso", a linguagem C ++ não impede que o objeto que fornece o vazamento do bloco RAII, portanto, o bloco RAII pode nunca ser feito .
Porque eles gerenciam os IDs de recursos de outras maneiras, conforme mencionado acima.
Porque eles gerenciam os IDs de recursos de outras maneiras, conforme mencionado acima.
Porque eles gerenciam os IDs de recursos de outras maneiras, conforme mencionado acima.
Java não tem destruidores.
Os documentos Java mencionam :
..mas colocar o código de gerenciamento de identificação de recurso
Object.finalizer
é amplamente considerado um antipadrão ( cf. ). Esse código deve ser escrito no site da chamada.Para as pessoas que usam o antipadrão, a justificativa é que elas podem ter esquecido de liberar os IDs de recurso no site de chamada. Assim, eles fazem isso novamente no finalizador, apenas por precaução.
Não há muitos casos de uso para finalizadores, pois eles são para executar um pedaço de código entre o momento em que não há mais referências fortes ao objeto e o momento em que sua memória é recuperada pelo GC.
Um possível caso de uso é quando você deseja manter um registro do tempo entre o objeto coletado pelo GC e o momento em que não há mais nenhuma referência forte ao objeto, como tal:
fonte
encontrou uma referência a isso no Dr. Dobbs wrt c ++ que tem idéias mais gerais que argumentam que os destruidores são problemáticos em uma linguagem em que são implementados. uma idéia aproximada aqui parece ser que o principal objetivo dos destruidores é lidar com a desalocação de memória, e isso é difícil de realizar corretamente. a memória é alocada por partes, mas objetos diferentes são conectados e as responsabilidades / limites da desalocação não são tão claras.
portanto, a solução para isso de um coletor de lixo evoluiu anos atrás, mas a coleta de lixo não se baseia em objetos que desaparecem do escopo nas saídas do método (que é uma idéia conceitual difícil de implementar), mas em um coletor executando periodicamente, de maneira um tanto indeterminada, quando o aplicativo tiver "pressão de memória" (ou seja, ficar sem memória).
em outras palavras, o mero conceito humano de um "objeto recentemente não utilizado" é, de certo modo, uma abstração enganosa no sentido de que nenhum objeto pode "instantaneamente" tornar-se não utilizado. objetos não utilizados só podem ser "descobertos" executando um algoritmo de coleta de lixo que atravessa o gráfico de referência de objeto e os algoritmos com melhor desempenho são executados de forma intermitente.
é possível que um algoritmo de coleta de lixo melhor esteja esperando para ser descoberto, capaz de identificar quase instantaneamente objetos não utilizados, o que poderia levar a um código de chamada de destruidor consistente, mas um não foi encontrado após muitos anos de pesquisa na área.
a solução para áreas de gerenciamento de recursos, como arquivos ou conexões, parece ter "gerenciadores" de objetos que tentam lidar com seu uso.
fonte