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?
java
garbage-collection
moonman239
fonte
fonte
Respostas:
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.
fonte
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.
fonte
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:
(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
malloc
para 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 ).
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).
fonte
finalize
de nenhum gerenciamento de recursos (de identificadores de arquivo, conexões de banco de dados, recursos de gpu etc.).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.
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:
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.
fonte
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.
fonte
Embora tenha
finalize
sido 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:
Invalide referências fracas que identificam objetos que não possuem nenhuma referência fortemente alcançável associada a eles.
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.
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.
fonte
Deixe-me sugerir uma reformulação e generalização da sua pergunta:
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
finalize
mé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.fonte
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.
fonte
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.
fonte
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.
fonte
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.
fonte