Por que os objetos Java não são excluídos imediatamente depois que não são mais referenciados?

77

Em Java, assim que um objeto não possui mais referências, ele se torna elegível para exclusão, mas a JVM decide quando o objeto é realmente excluído. Para usar a terminologia Objective-C, todas as referências Java são inerentemente "fortes". No entanto, no Objective-C, se um objeto não tiver mais referências fortes, ele será excluído imediatamente. Por que não é esse o caso em Java?

moonman239
fonte
46
Você não deve se importar quando objetos Java forem realmente excluídos. É um detalhe de implementação.
Basile Starynkevitch
154
@BasileStarynkevitch Você deve absolutamente se importar e desafiar o funcionamento do seu sistema / plataforma. Fazer perguntas 'como' e 'por que' é uma das melhores maneiras de se tornar um melhor programador (e, em um sentido mais geral, uma pessoa mais inteligente).
Artur Biesiadowski
6
O que o Objetivo C faz quando há referências circulares? Eu suponho que apenas vaza eles?
Mehrdad
45
@ArturBiesiadowksi: Não, a especificação Java não informa quando um objeto é excluído (e da mesma forma, para o R5RS ). Você poderia e provavelmente deveria desenvolver seu programa Java como se essa exclusão nunca acontecesse (e para processos de curta duração como um mundo olá Java, isso realmente não acontece). Você pode se importar com o conjunto de objetos vivos (ou com o consumo de memória), que é uma história diferente.
Basile Starynkevitch
28
Um dia, o novato disse ao mestre: "Tenho uma solução para o nosso problema de alocação. Daremos a cada alocação uma contagem de referência e, quando chegar a zero, podemos excluir o objeto". O mestre respondeu "Um dia o novato disse ao mestre" Eu tenho uma solução ...
Eric Lippert

Respostas:

79

Antes de tudo, o Java tem referências fracas e outra categoria de melhor esforço chamada referências flexíveis. Referências fracas vs. fortes é um problema completamente separado da contagem de referências versus coleta de lixo.

Segundo, existem padrões no uso da memória que podem tornar a coleta de lixo mais eficiente no tempo, sacrificando o espaço. Por exemplo, os objetos mais recentes têm muito mais probabilidade de serem excluídos do que os objetos mais antigos. Portanto, se você esperar um pouco entre as varreduras, poderá excluir a maior parte da nova geração de memória, enquanto move os poucos sobreviventes para um armazenamento de longo prazo. Esse armazenamento a longo prazo pode ser verificado com muito menos frequência. A exclusão imediata via gerenciamento manual da memória ou contagem de referência é muito mais propensa à fragmentação.

É como a diferença entre ir às compras no mercado uma vez por salário e ir todos os dias para obter comida suficiente para um dia. Sua única grande viagem levará muito mais tempo do que uma pequena viagem individual, mas no geral você acaba economizando tempo e provavelmente dinheiro.

Karl Bielefeldt
fonte
58
A esposa de um programador o envia ao supermercado. Ela diz a ele: "Compre um pedaço de pão e, se vir alguns ovos, pegue uma dúzia". Mais tarde, o programador retorna com uma dúzia de pães debaixo do braço.
819 Neil
7
Sugiro mencionar que o novo tempo de geração gc é geralmente proporcional à quantidade de vivos objetos, assim que ter objetos mais apagados significa que seu custo não será pago a todos em muitos casos. A exclusão é tão simples quanto inverter o ponteiro do espaço sobrevivente e, opcionalmente, zerar todo o espaço da memória em um grande conjunto memset (não tenho certeza se é feito no final de gc ou amortizado durante a alocação de tlabs ou objetos em jvms atuais)
Artur Biesiadowski
64
@ Neil que não deveria ser 13 pães?
JAD
67
"Desativado por um erro no corredor 7"
joeytwiddle
13
@JAD Eu teria dito 13, mas a maioria não costuma entender isso. ;)
Neil
86

Porque saber corretamente que algo não é mais referenciado não é fácil. Nem perto de fácil.

E se você tiver dois objetos fazendo referência um ao outro? Eles ficam para sempre? Estendendo essa linha de pensamento para resolver qualquer estrutura de dados arbitrária, você verá em breve por que a JVM ou outros coletores de lixo são forçados a empregar métodos muito mais sofisticados para determinar o que ainda é necessário e o que pode ser feito.

whatsisname
fonte
7
Ou você pode adotar uma abordagem Python em que utiliza a recontagem o máximo possível, recorrendo a um GC quando espera que haja dependências circulares com vazamento de memória. Não vejo por que eles não poderiam contar novamente além do GC?
Mehrdad
27
@Mehrdad Eles poderiam. Mas provavelmente seria mais lento. Nada impede você de implementar isso, mas não espere vencer nenhum dos GCs no Hotspot ou no OpenJ9.
Josef
21
@ jpmc26 porque se você excluir objetos assim que eles não forem mais usados, a probabilidade será alta, você os excluirá em uma situação de alta carga, o que aumentará ainda mais a carga. O GC pode ser executado quando houver menos carga. A contagem de referência em si é uma pequena sobrecarga para todas as referências. Além disso, com um GC, você pode descartar uma grande parte da memória sem referências sem manipular os objetos únicos.
Josef
33
@ Josef: a contagem adequada de referências também não é gratuita; a atualização da contagem de referência requer incrementos / decretos atômicos, que são surpreendentemente caros , especialmente em arquiteturas multicore modernas. No CPython, isso não é muito problemático (o CPython é extremamente lento por si só, e o GIL limita seu desempenho multithread a níveis de núcleo único), mas em uma linguagem mais rápida que também suporta paralelismo, pode ser um problema. Não é uma chance do PyPy se livrar completamente da contagem de referências e usar apenas o GC.
Matteo Italia
10
@Mehrdad, depois de implementar seu GC de contagem de referência para Java, terei prazer em testá-lo para encontrar um caso em que ele tenha desempenho pior do que qualquer outra implementação de GC.
Josef
45

AFAIK, a especificação da JVM (escrita em inglês) não menciona quando exatamente um objeto (ou um valor) deve ser excluído e deixa isso para a implementação (da mesma forma para o R5RS ). De alguma forma, requer ou sugere um coletor de lixo, mas deixa os detalhes para a implementação. E da mesma forma para a especificação Java.

Lembre-se de que linguagens de programação são especificações (de sintaxe , semântica , etc ...), não implementações de software. Uma linguagem como Java (ou sua JVM) possui muitas implementações. Sua especificação é publicada , pode ser baixada (para que você possa estudá-la) e escrita em inglês. §2.5.3 O monte de especificações da JVM menciona um coletor de lixo:

O armazenamento de heap para objetos é recuperado por um sistema de gerenciamento de armazenamento automático (conhecido como coletor de lixo); objetos nunca são desalocados explicitamente. A Java Virtual Machine não assume nenhum tipo específico de sistema de gerenciamento automático de armazenamento

(a ênfase é minha; a finalização BTW é mencionada no §12.6 da especificação Java e um modelo de memória está no §17.4 da especificação Java)

Portanto (em Java), você não deve se importar quando um objeto é excluído e pode codificar como se isso não acontecesse (raciocinando em uma abstração na qual você ignora isso). É claro que você precisa se preocupar com o consumo de memória e o conjunto de objetos vivos, o que é uma pergunta diferente . Em vários casos simples (pense em um programa "olá mundo"), você é capaz de provar - ou se convencer - que a memória alocada é bastante pequena (por exemplo, menos de um gigabyte) e então não se importa com nada exclusão de objetos individuais . Em mais casos, você pode se convencer de que os objetos vivos(ou alcançáveis, que é um superconjunto - mais fácil de raciocinar - sobre os vivos) nunca excede um limite razoável (e então você depende do GC, mas não se importa como e quando a coleta de lixo acontece). Leia sobre a complexidade do espaço .

Eu acho que em várias implementações da JVM executando um programa Java de curta duração como o hello world, o coletor de lixo não é acionado e nenhuma exclusão ocorre. AFAIU, esse comportamento está em conformidade com as inúmeras especificações Java.

A maioria das implementações da JVM usa técnicas de cópia geracional (pelo menos para a maioria dos objetos Java, aqueles que não usam finalização ou referências fracas ; e a finalização não é garantida que acontece em pouco tempo e pode ser adiada, portanto, é apenas um recurso útil que seu código não deve depende muito disso) em que a noção de excluir um objeto individual não faz sentido (uma vez que um grande bloco de memória - que contém zonas de memória para muitos objetos -, talvez vários megabytes ao mesmo tempo, é liberado ao mesmo tempo).

Se a especificação da JVM exigisse que cada objeto fosse excluído exatamente o mais rápido possível (ou simplesmente colocasse mais restrições na exclusão do objeto), técnicas de GC geracionais eficientes seriam proibidas e os projetistas de Java e da JVM teriam sido prudentes em evitar isso.

BTW, pode ser possível que uma JVM ingênua que nunca exclua objetos e não libere memória possa estar em conformidade com as especificações (a letra, não o espírito) e certamente possa executar uma coisa de olá mundo na prática (observe que a maioria programas Java minúsculos e de curta duração provavelmente não alocam mais do que alguns gigabytes de memória). É claro que essa JVM não vale a pena mencionar e é apenas uma coisa de brinquedo (como é essa implementação de mallocpara C). Consulte o Epsilon NoOp GC para obter mais informações. As JVMs da vida real são peças de software muito complexas e misturam várias técnicas de coleta de lixo.

Além disso, Java não é o mesmo que a JVM e você tem implementações em Java em execução sem a JVM (por exemplo , compiladores Java antecipados , tempo de execução do Android ). Em alguns casos (principalmente os acadêmicos), você pode imaginar (chamadas técnicas de "coleta de lixo em tempo de compilação") que um programa Java não aloca ou exclui em tempo de execução (por exemplo, porque o compilador otimizador foi inteligente o suficiente para usar apenas o pilha de chamadas e variáveis ​​automáticas ).

Por que os objetos Java não são excluídos imediatamente depois que não são mais referenciados?

Porque as especificações Java e JVM não exigem isso.


Leia o manual do GC para obter mais informações (e as especificações da JVM ). Observe que estar vivo (ou útil para computação futura) de um objeto é uma propriedade de todo o programa (não modular).

O Objective-C favorece uma abordagem de contagem de referência para o gerenciamento de memória . E que também tem armadilhas (por exemplo, o Objective-C programador tem de se preocupar com referências circulares por explicitando referências fracas, mas a JVM lida com referências circulares bem na prática, sem a necessidade de atenção do programador Java).

Não há nenhuma bala de prata na programação e no design da linguagem de programação (esteja ciente do problema da parada ; ser um objeto vivo útil é indecidível em geral).

Você também pode ler SICP , Pragmática da Linguagem de Programação , o Dragon Book , o Lisp em Pequenos Pedaços e Sistemas Operacionais: Três Pedaços Fáceis . Eles não são sobre Java, mas abrirão sua mente e ajudarão a entender o que uma JVM deve fazer e como ela pode praticamente funcionar (com outras peças) no seu computador. Você também pode passar muitos meses (ou vários anos) estudando o código-fonte complexo das implementações de JVM de código aberto existentes (como o OpenJDK , que possui vários milhões de linhas de código-fonte).

Basile Starynkevitch
fonte
20
"pode ​​ser possível que uma JVM ingênua que nunca exclua objetos e não libere memória possa estar em conformidade com as especificações" Certamente está em conformidade com as especificações! Na verdade, o Java 11 está adicionando um coletor de lixo não operacional para, entre outras coisas, programas de curta duração.
Michael
6
"você não deve se importar quando um objeto for excluído" Discordo. Por um lado, você deve saber que RAII não é mais um padrão viável e que não pode depender finalizede nenhum gerenciamento de recursos (de identificadores de arquivo, conexões de banco de dados, recursos de gpu etc.).
Alexander
4
@ Michael Faz todo o sentido para o processamento em lote com um limite de memória usado. O sistema operacional pode apenas dizer "toda a memória usada por este programa agora se foi!" afinal, o que é bastante rápido. De fato, muitos programas em C foram escritos dessa maneira, especialmente no mundo Unix. Pascal teve o lindamente horrível "redefinir o ponteiro da pilha / pilha para um ponto de verificação pré-salvo" que lhe permitia fazer a mesma coisa, embora fosse bastante inseguro - marcar, iniciar subtarefa, redefinir.
Luaan
6
@Alexander em geral fora do C ++ (e algumas linguagens que dele derivam intencionalmente), supondo que o RAII funcione apenas com finalizadores é um anti-padrão, que deve ser avisado e substituído por um bloco explícito de controle de recursos. O ponto principal do GC é que a vida útil e os recursos são dissociados, afinal.
Leushenko
3
@ Leushenko Eu discordo totalmente que "a vida e os recursos estão dissociados" é o "ponto principal" do GC. É o preço negativo que você paga pelo principal ponto do GC: gerenciamento de memória fácil e seguro. "Supondo que o RAII funcione apenas com finalizadores, é um anti-padrão" Em Java? Possivelmente. Mas não no CPython, Rust, Swift ou Objective C. "alertaram e substituíram por um bloco explícito de controle de recursos" Não, eles são estritamente mais limitados. Um objeto que gerencia um recurso por meio do RAII fornece uma alça para passar a vida do escopo. Um bloco de tentativa com recurso é limitado a um único escopo.
Alexander
23

Para usar a terminologia Objective-C, todas as referências Java são inerentemente "fortes".

Isso não está correto - o Java tem referências fracas e flexíveis, embora elas sejam implementadas no nível do objeto e não como palavras-chave da linguagem.

No Objective-C, se um objeto não tiver mais referências fortes, ele será excluído imediatamente.

Isso também não é necessariamente correto - algumas versões do Objective C realmente usavam um coletor de lixo de gerações. Outras versões não tinham coleta de lixo.

É verdade que as versões mais recentes do Objective C usam a contagem automática de referência (ARC) em vez de um GC baseado em rastreamento, e isso (geralmente) resulta no objeto "excluído" quando a contagem de referência atinge zero. No entanto, observe que uma implementação da JVM também pode ser compatível e funcionar exatamente dessa maneira (heck, poderia ser compatível e não ter GC).

Então, por que a maioria das implementações da JVM não faz isso e, em vez disso, usa algoritmos de GC baseados em rastreamento?

Simplificando, o ARC não é tão utópico quanto parece:

  • Você precisa incrementar ou diminuir um contador sempre que uma referência é copiada, modificada ou sai do escopo, o que gera uma sobrecarga de desempenho óbvia.
  • O ARC não pode limpar facilmente as referências cíclicas, pois todas têm uma referência uma à outra, portanto, sua contagem de referências nunca chega a zero.

O ARC tem vantagens, é claro - é simples de implementar e coletar é determinístico. Mas as desvantagens acima, entre outras, são a razão pela qual a maioria das implementações da JVM usará um GC geracional baseado em rastreamento.

berry120
fonte
1
O engraçado é que a Apple mudou para o ARC precisamente porque viu que, na prática, supera amplamente outros GCs (em particular os geracionais). Para ser justo, isso ocorre principalmente em plataformas com restrição de memória (iPhone). Mas eu refutaria sua afirmação de que “o ARC não é tão utópico quanto parece” ao dizer que os GCs geracionais (e outros não determinísticos) não são tão utópicos quanto parecem à primeira vista: a destruição determinística é provavelmente uma opção melhor no mundo. grande maioria dos cenários.
Konrad Rudolph
3
@KonradRudolph Embora eu também seja fã de destruição determinística, não acho que “a melhor opção na grande maioria dos cenários” se sustenta. É certamente uma opção melhor quando a latência ou a memória é mais importante que a taxa de transferência média e, em particular, quando a lógica é razoavelmente simples. Mas não é como se não houvesse muitos aplicativos complexos que exijam muitas referências cíclicas etc. e exijam uma operação média rápida, mas realmente não se importam com a latência e têm muita memória disponível. Para estes, é duvidoso que o ARC seja uma boa ideia.
leftaroundabout
1
@leftaroundabout Na “maioria dos cenários”, nem a taxa de transferência nem a pressão da memória são um gargalo, portanto, não importa. Seu exemplo é um cenário específico. Concedido, não é extremamente incomum, mas eu não chegaria ao ponto de afirmar que é mais comum do que outros cenários em que o ARC é mais adequado. Além disso, o ARC pode lidar com ciclos muito bem. Requer apenas alguma intervenção manual simples do programador. Isso o torna menos ideal, mas dificilmente quebra um acordo. Eu afirmo que a finalização determinística é uma característica muito mais importante do que você imagina.
Konrad Rudolph
3
@KonradRudolph Se o ARC exigir alguma intervenção manual simples do programador, ele não lida com ciclos. Se você começar a usar fortemente as listas duplamente vinculadas, o ARC retornará à alocação manual de memória. Se você tiver grandes gráficos arbitrários, o ARC obriga a escrever um coletor de lixo. O argumento do GC seria que os recursos que precisam ser destruídos não são o trabalho do subsistema de memória e, para rastrear os relativamente poucos deles, eles devem ser finalizados de maneira determinística por meio de alguma intervenção manual simples do programador.
Prosfilaes
2
O @KonradRudolph ARC e os ciclos levam fundamentalmente a vazamentos de memória se não forem manipulados manualmente. Em sistemas suficientemente complexos, podem ocorrer grandes vazamentos se, por exemplo, algum objeto armazenado em um mapa armazenar uma referência a esse mapa, uma alteração que poderia ser feita por um programador não responsável pelas seções do código que cria e destrói esse mapa. Gráficos arbitrários grandes não significam que os ponteiros internos não sejam fortes, que não há problema em os itens vinculados desaparecerem. Se lidar com alguns vazamentos de memória é um problema menor do que ter que fechar manualmente os arquivos, não vou dizer, mas é real.
Prosfilaes # 11/18
5

Java não especifica com precisão quando o objeto é coletado, pois isso dá às implementações a liberdade de escolher como lidar com a coleta de lixo.

Existem muitos mecanismos diferentes de coleta de lixo, mas aqueles que garantem a coleta imediata de um objeto são quase inteiramente baseados na contagem de referências (não conheço nenhum algoritmo que quebre essa tendência). A contagem de referência é uma ferramenta poderosa, mas tem um custo de manutenção da contagem de referência. No código de leitura única, isso nada mais é do que um incremento e decremento; portanto, atribuir um ponteiro pode custar um custo da ordem de 3x mais no código contado de referência do que no código contado de não referência (se o compilador puder fazer tudo da máquina código).

No código multithread, o custo é maior. Ele exige incrementos / decrementos atômicos ou bloqueios, os quais podem ser caros. Em um processador moderno, uma operação atômica pode ser da ordem de 20x mais cara que uma simples operação de registro (obviamente varia de processador para processador). Isso pode aumentar o custo.

Portanto, com isso, podemos considerar as compensações feitas por vários modelos.

  • O Objective-C foca no ARC - contagem de referência automatizada. A abordagem deles é usar a contagem de referência para tudo. Não há detecção de ciclo (que eu saiba); portanto, espera-se que os programadores impeçam a ocorrência de ciclos, o que custa tempo de desenvolvimento. Sua teoria é que os ponteiros não são atribuídos com tanta frequência, e seu compilador pode identificar situações em que o incremento / decremento da contagem de referências não pode causar a morte de um objeto e eliminar completamente esses incrementos / decrementos. Assim, eles minimizam o custo da contagem de referência.

  • O CPython usa um mecanismo híbrido. Eles usam contagens de referência, mas também possuem um coletor de lixo que identifica os ciclos e os libera. Isso fornece os benefícios dos dois mundos, ao custo de ambas as abordagens. O CPython deve manter contagens de referência efaça a contabilidade para detectar ciclos. O CPython se livra disso de duas maneiras. O problema é que o CPython não é realmente totalmente multithread. Possui um bloqueio conhecido como GIL, que limita o multithreading. Isso significa que o CPython pode usar incrementos / decrementos normais em vez de atômicos, o que é muito mais rápido. O CPython também é interpretado, o que significa que operações como a atribuição a uma variável já precisam de um punhado de instruções em vez de apenas 1. O custo extra de fazer os incrementos / decrementos, que são feitos rapidamente no código C, é menos problemático porque nós ' já paguei esse custo.

  • Java segue a abordagem de não garantir um sistema de referência contado. De fato, a especificação não diz nada sobre como os objetos são gerenciados, exceto que haverá um sistema de gerenciamento de armazenamento automático. No entanto, a especificação também sugere fortemente a suposição de que esse lixo será coletado de maneira a lidar com os ciclos. Ao não especificar quando os objetos expiram, o java obtém a liberdade de usar coletores que não perdem tempo incrementando / diminuindo. De fato, algoritmos inteligentes, como coletores de lixo geracionais, podem até lidar com muitos casos simples, sem sequer olhar para os dados que estão sendo recuperados (eles apenas precisam olhar para os dados que ainda estão sendo referenciados).

Então, podemos ver que cada um desses três teve que fazer trocas. Qual tradeoff é o melhor depende muito da natureza de como o idioma deve ser usado.

Cort Ammon
fonte
4

Embora tenha finalizesido copiado no GC do Java, a coleta de lixo em sua essência não está interessada em objetos mortos, mas em objetos vivos. Em alguns sistemas de GC (possivelmente incluindo algumas implementações de Java), a única coisa que distingue um monte de bits que representa um objeto de um monte de armazenamento que não é usado para nada pode ser a existência de referências ao primeiro. Enquanto objetos com finalizadores são adicionados a uma lista especial, outros objetos podem não ter nada em qualquer lugar do universo que diga que seu armazenamento está associado a um objeto, exceto pelas referências mantidas no código do usuário. Quando a última referência desse tipo for substituída, o padrão de bits na memória deixará imediatamente de ser reconhecido como um objeto, independentemente de alguma coisa no universo estar ciente disso.

O objetivo da coleta de lixo não é destruir objetos aos quais não existem referências, mas realizar três coisas:

  1. Invalide referências fracas que identificam objetos que não possuem nenhuma referência fortemente alcançável associada a eles.

  2. Pesquise a lista de objetos do sistema com finalizadores para ver se algum deles não possui nenhuma referência fortemente alcançável associada a eles.

  3. Identifique e consolide regiões de armazenamento que não estão sendo usadas por nenhum objeto.

Observe que o objetivo principal do GC é o número 3 e, quanto mais se esperar antes de fazê-lo, mais chances de consolidação haverá. Faz sentido fazer o nº 3 nos casos em que alguém teria um uso imediato para o armazenamento, mas, caso contrário, faz mais sentido adiá-lo.

supercat
fonte
5
Na verdade, o gc tem apenas um objetivo: simular memória infinita. Tudo o que você nomeou como objetivo é uma imperfeição na abstração ou um detalhe da implementação.
Deduplicator
@ Reduplicador: referências fracas oferecem semântica útil que não pode ser alcançada sem a assistência do GC.
Supercat
Claro, referências fracas têm semântica útil. Mas essas semânticas seriam necessárias se a simulação fosse melhor?
Deduplicator
@Duplicador: Sim. Considere uma coleção que define como as atualizações irão interagir com a enumeração. Essa coleção pode precisar conter referências fracas a qualquer enumerador ativo. Em um sistema de memória ilimitada, uma coleção repetida repetidamente faria sua lista de enumeradores interessados ​​crescer sem limites. A memória necessária para essa lista não seria um problema, mas o tempo necessário para iterá-la prejudicaria o desempenho do sistema. Adicionar GC pode significar a diferença entre um algoritmo O (N) e O (N ^ 2).
Supercat
2
Por que você deseja notificar os enumeradores, em vez de anexar a uma lista e deixá-los procurar por si mesmos quando são usados? E qualquer programa que dependa do lixo que está sendo processado em tempo hábil, em vez de depender da pressão da memória, está vivendo em estado de pecado de qualquer maneira, se é que se move.
Deduplicator
4

Deixe-me sugerir uma reformulação e generalização da sua pergunta:

Por que o Java não garante muito seu processo de GC?

Com isso em mente, faça uma rápida rolagem pelas respostas aqui. Existem sete até agora (sem contar este), com alguns tópicos de comentários.

Essa é a sua resposta.

GC é difícil. Há muitas considerações, muitas compensações diferentes e, finalmente, muitas abordagens muito diferentes. Algumas dessas abordagens tornam viável a GC um objeto assim que não é necessário; outros não. Mantendo o contrato livre, o Java oferece aos implementadores mais opções.

É claro que existe uma troca nessa decisão: é claro, mantendo o contrato livre, o Java principalmente * tira a capacidade dos programadores de confiar em destruidores. Isso é algo que os programadores de C ++ em particular geralmente sentem falta ([citação necessário];)), portanto não é uma troca insignificante. Não vi uma discussão sobre essa meta-decisão em particular, mas presumivelmente o pessoal de Java decidiu que os benefícios de ter mais opções de GC superavam os benefícios de poder dizer aos programadores exatamente quando um objeto será destruído.


* Existe o finalizemétodo, mas por várias razões que estão fora do escopo desta resposta, é difícil e não é uma boa ideia confiar nele.

yshavit
fonte
3

Existem duas estratégias diferentes de manipulação de memória sem código explícito escrito pelo desenvolvedor: coleta de lixo e contagem de referência.

A coleta de lixo tem a vantagem de "funcionar", a menos que o desenvolvedor faça algo estúpido. Com a contagem de referência, você pode ter ciclos de referência, o que significa que "funciona", mas o desenvolvedor às vezes precisa ser inteligente. Então isso é uma vantagem para a coleta de lixo.

Com a contagem de referência, o objeto desaparece imediatamente quando a contagem de referência cai para zero. Essa é uma vantagem para a contagem de referência.

No sentido rápido, a coleta de lixo é mais rápida se você acredita nos fãs da coleta de lixo e a contagem de referência é mais rápida se você acredita nos fãs da contagem de referência.

São apenas dois métodos diferentes para alcançar o mesmo objetivo: o Java escolheu um método, o Objective-C escolheu outro (e adicionou muito suporte ao compilador para alterá-lo de um pé no saco para algo que é pouco trabalhoso para desenvolvedores).

Alterar o Java da coleta de lixo para a contagem de referência seria uma tarefa importante, pois seriam necessárias muitas alterações no código.

Em teoria, Java poderia ter implementado uma mistura de coleta de lixo e contagem de referência: se a contagem de referência for 0, o objeto estará inacessível, mas não necessariamente o contrário. Portanto, você pode manter as contagens de referência e excluir objetos quando a contagem de referência for zero (e depois executar a coleta de lixo de tempos em tempos para capturar objetos em ciclos de referência inacessíveis). Eu acho que o mundo está dividido em 50/50 em pessoas que pensam que adicionar uma contagem de referência à coleta de lixo é uma má idéia, e pessoas que pensam que adicionar uma coleta de lixo à contagem de referência é uma má idéia. Então isso não vai acontecer.

Portanto, o Java pode excluir objetos imediatamente se a contagem de referência se tornar zero e excluir objetos dentro de ciclos inacessíveis posteriormente. Mas essa é uma decisão de design, e o Java decidiu contra.

gnasher729
fonte
Com a contagem de referências, a finalização é trivial, pois o programador cuidava dos ciclos. Com o gc, os ciclos são triviais, mas o programador precisa ter cuidado ao finalizar.
Deduplicator
@Deduplicator Em Java, também é possível criar fortes referências a objetos que estão sendo finalizados ... Em Objective-C e Swift, uma vez que a contagem de referência é zero, o objeto irá desaparecer (a menos que você colocar um loop infinito em dealloc / deísta).
precisa saber é o seguinte
Só notei verificador ortográfico estúpido substituindo deinit com deísta ...
gnasher729
1
Há uma razão para a maioria dos programadores odeia a correção ortográfica automática ... ;-)
Deduplicator
lol ... Eu acho que o mundo está dividido em 0,1 / 0,1 / 99,8 entre pessoas que pensam que adicionar uma contagem de referência à coleta de lixo é uma má idéia, e pessoas que pensam que adicionar uma coleta de lixo à contagem de referência é uma má idéia, e pessoas que manter contando os dias até que a coleta de lixo chega porque que tonelada já está recebendo mau cheiro de novo ...
leftaroundabout
1

Todos os outros argumentos e discussões de desempenho sobre a dificuldade de entender quando não há mais referências a um objeto estão corretos, embora outra idéia que eu acho que vale a pena mencionar é que há pelo menos uma JVM (azul) que considera algo como isto na medida em que implementa o gc paralelo, que essencialmente possui um thread vm, verificando constantemente as referências para tentar excluí-las, que agirão de maneira totalmente diferente do que você está falando. Basicamente, ele examinará constantemente a pilha e tentará recuperar qualquer memória que não esteja sendo referenciada. Isso incorre em um custo de desempenho muito pequeno, mas leva a essencialmente zero ou muito tempo de GC. (Isto é, a menos que o tamanho da pilha em constante expansão exceda a RAM do sistema e depois o Azul fique confuso e depois haja dragões)

TLDR Algo assim existe para a JVM, é apenas uma jvm especial e possui desvantagens como qualquer outro compromisso de engenharia.

Isenção de responsabilidade: não tenho vínculos com a Azul, apenas a usamos em um trabalho anterior.

ford prefeito
fonte
1

A maximização da taxa de transferência sustentada ou a minimização da latência de gc estão em tensão dinâmica, que é provavelmente o motivo mais comum pelo qual o GC não ocorre imediatamente. Em alguns sistemas, como os aplicativos de emergência 911, não atingir um limite de latência específico pode começar a desencadear processos de failover do site. Em outros, como um site bancário e / ou de arbitragem, é muito mais importante maximizar a taxa de transferência.

barmid
fonte
0

Rapidez

Por que tudo isso está acontecendo, em última análise, é devido à velocidade. Se os processadores eram infinitamente rápidos, ou (para ser prático) próximos, por exemplo, 1.000.000.000.000.000.000.000.000.000.000.000 operações por segundo, então você pode ter coisas incrivelmente longas e complicadas acontecendo entre cada operador, como garantir que os objetos não referenciados sejam excluídos. Como esse número de operações por segundo atualmente não é verdadeiro e, como a maioria das outras respostas explica, na verdade é complicado e exige muitos recursos para descobrir isso, existe uma coleta de lixo para que os programas possam se concentrar naquilo que estão realmente tentando obter. maneira rápida.

Michael Durrant
fonte
Bem, tenho certeza de que encontraríamos maneiras mais interessantes de usar os ciclos extras do que isso.
Deduplicator