A coleta de lixo do Java cuida de objetos mortos na pilha, mas congela o mundo algumas vezes. Em C ++, tenho que ligar delete
para descartar um objeto criado no final de seu ciclo de vida.
Este delete
parece ser um preço muito baixo para pagar por ambiente não-congelamento. A colocação de todas as delete
palavras-chave relevantes é uma tarefa mecânica. Pode-se escrever um script que viaje pelo código e exclua uma vez que nenhuma nova ramificação use um determinado objeto.
Portanto, quais são os prós e os contras do Java build in vs C ++ diy model of garbage collection.
Não quero iniciar um thread C ++ vs Java. Minha pergunta é diferente.
Essa coisa toda do GC - tudo se resume a "apenas ser elegante, não se esqueça de excluir objetos que você criou - e você não precisará de nenhum GC dedicado? Ou é mais parecido com" descartar objetos em C ++ é realmente complicado - eu gastar 20% do meu tempo nisso e, no entanto, vazamentos de memória são um lugar comum "?
fonte
new
entra no construtor da sua classe que gerencia a memória,delete
entra no destruidor. A partir daí, é tudo automático (armazenamento). Note que isso funciona para todos os tipos de recursos, não apenas para memória, ao contrário da coleta de lixo. Mutexes são obtidos em um construtor, liberados em um destruidor. Os arquivos são abertos em um construtor, fechados em um destruidor.Respostas:
O ciclo de vida do objeto C ++
Se você criar objetos locais, não precisará excluí-los: o compilador gera código para excluí-los automaticamente quando o objeto sai do escopo
Se você usar ponteiros de objetos e criar objetos no armazenamento gratuito, será necessário excluir o objeto quando ele não for mais necessário (como você descreveu). Infelizmente, em softwares complexos, isso pode ser muito mais desafiador do que parece (por exemplo, e se uma exceção for levantada e a parte de exclusão esperada nunca for alcançada?).
Felizmente, no C ++ moderno (também conhecido como C ++ 11 e posterior), você tem indicadores inteligentes, como por exemplo
shared_ptr
. Eles fazem a contagem de referência do objeto criado de uma maneira muito eficiente, um pouco como um coletor de lixo faria em Java. E assim que o objeto não for mais referenciado, o último ativoshared_ptr
excluirá o objeto para você. Automaticamente. Como um coletor de lixo, mas um objeto de cada vez e sem demora (Ok: você precisa de alguns cuidados extras eweak_ptr
lidar com referências circulares).Conclusão: hoje em dia você pode escrever código C ++ sem se preocupar com a alocação de memória e que é tão livre de vazamentos quanto com um GC, mas sem o efeito de congelamento.
O ciclo de vida do objeto Java
O bom é que você não precisa se preocupar com o ciclo de vida do objeto. Você apenas os cria e o java cuida do resto. Um GC moderno identificará e destruirá os objetos que não são mais necessários (inclusive se houver referências circulares entre objetos mortos).
Infelizmente, devido a esse conforto, você não tem controle real de quando o objeto é realmente excluído . Semanticamente, a exclusão / destruição coincide com a coleta de lixo .
Isso é perfeitamente bom se você olhar objetos apenas em termos de memória. Exceto pelo congelamento, mas estes não são fatais (as pessoas estão trabalhando nisso). Eu não sou especialista em Java, mas acho que a destruição atrasada torna mais difícil identificar vazamentos em java devido a referências mantidas acidentalmente, apesar de os objetos não serem mais necessários (ou seja, você não pode realmente monitorar a exclusão de objetos).
Mas e se o objeto tiver que controlar outros recursos além da memória, por exemplo, um arquivo aberto, um semáforo, um serviço do sistema? Sua classe deve fornecer um método para liberar esses recursos. E você terá a responsabilidade de garantir que esse método seja chamado quando os recursos não forem mais necessários. Em todo caminho de ramificação possível através do seu código, garantindo que ele também seja chamado em caso de exceções. O desafio é muito semelhante à exclusão explícita em C ++.
Conclusão: o GC resolve um problema de gerenciamento de memória. Mas não trata do gerenciamento de outros recursos do sistema. A ausência de exclusão "just-in-time" pode tornar o gerenciamento de recursos muito desafiador.
Exclusão, coleta de lixo e RAII
Quando você pode controlar a exclusão de um objeto e o destruidor que deve ser chamado na exclusão, você pode se beneficiar da RAII . Essa abordagem vê a memória apenas como um caso especial de alocação de recursos e vincula o gerenciamento de recursos com mais segurança ao ciclo de vida do objeto, garantindo assim o uso estritamente controlado dos recursos.
fonte
new
fora dos construtores / ponteiros inteligentes e nunca usedelete
fora de um destruidor.Like garbage collector, but one object at a time and without delay (Ok: you need some extra care and weak_ptr to cope with circular references).
Contagem de referência pode cascata embora. Por exemplo, a última referência aA
desaparece, masA
também tem a última referência aB
, quem tem a última referência aC
...And you'll have the responsibility to make sure that this method is called when the resources are no longer needed. In every possible branching path through your code, ensuring it is also invoked in case of exceptions.
Java e C # têm instruções de bloco especiais para isso.Se você pode escrever um roteiro como esse, parabéns. Você é um desenvolvedor melhor do que eu. De longe.
A única maneira de você realmente evitar vazamentos de memória em casos práticos é padrões de codificação muito rígidos, com regras muito rígidas, que são as proprietárias de um objeto, e quando ele pode e deve ser liberado, ou ferramentas como ponteiros inteligentes que contam referências a objetos e excluem objetos quando a última referência se foi.
fonte
Se você escreve o código C ++ correto com RAII, normalmente não escreve nenhum novo ou exclui. Os únicos "novos" que você escreve estão dentro de ponteiros compartilhados, para que você realmente nunca precise usar "excluir".
fonte
new
, mesmo com ponteiros compartilhados - você deveria estar usandostd::make_shared
.make_unique
. É realmente muito raro que você realmente precise de propriedade compartilhada.Facilitar a vida dos programadores e evitar vazamentos de memória é uma vantagem importante da coleta de lixo, mas não é a única. Outra é impedir a fragmentação da memória. No C ++, depois de alocar um objeto usando a
new
palavra-chave, ele permanece em uma posição fixa na memória. Isso significa que, conforme o aplicativo é executado, você acaba tendo lacunas de memória livre entre os objetos alocados. Portanto, a alocação de memória no C ++ deve, necessariamente, ser um processo mais complicado, pois o sistema operacional precisa encontrar blocos não alocados de determinado tamanho que se ajustem às lacunas.A coleta de lixo cuida disso, pegando todos os objetos que não são excluídos e deslocando-os na memória para formar um bloco contínuo. Se você perceber que a coleta de lixo leva algum tempo, provavelmente é por causa desse processo, não devido à desalocação de memória. O benefício disso é que, quando se trata de alocação de memória, é quase tão simples quanto mudar um ponteiro para o final da pilha.
Portanto, em C ++, a exclusão de objetos é rápida, mas sua criação pode ser lenta. No Java, a criação de objetos não leva tempo, mas você precisa fazer algumas tarefas de limpeza de vez em quando.
fonte
As principais promessas de Java foram
Parece que Java garante que o lixo será descartado (não necessariamente de maneira eficiente). Se você usa C / C ++, tem liberdade e responsabilidade. Você pode fazer isso melhor do que o GC do Java ou pode ser muito pior (pule
delete
todos juntos e tenha problemas de vazamento de memória).Se você precisar de um código que "atenda a certos padrões de qualidade" e para otimizar a "relação preço / qualidade", use Java. Se você estiver pronto para investir recursos extras (tempo de seus especialistas) para melhorar o desempenho de aplicativos de missão crítica - use C.
fonte
A grande diferença que a coleta de lixo faz não é que você não precise excluir objetos explicitamente. A diferença muito maior é que você não precisa copiar objetos.
Isso tem efeitos que se tornam difundidos no design de programas e interfaces em geral. Deixe-me dar apenas um pequeno exemplo para mostrar como isso é abrangente.
Em Java, quando você abre algo de uma pilha, o valor que está sendo exibido é retornado, para que você obtenha um código como este:
Em Java, isso é seguro para exceções, porque tudo o que realmente estamos fazendo é copiar uma referência a um objeto, que é garantido que isso aconteça sem uma exceção. O mesmo não é verdade em C ++. Em C ++, retornar um valor significa (ou pelo menos pode significar) copiar esse valor e com alguns tipos que podem gerar uma exceção. Se a exceção for lançada após o item ter sido removido da pilha, mas antes que a cópia chegue ao destinatário, o item vazou. Para evitar isso, a pilha do C ++ usa uma abordagem um pouco desajeitada, na qual recuperar o item superior e remover o item superior são duas operações separadas:
Se a primeira instrução gerar uma exceção, a segunda não será executada; portanto, se uma exceção for lançada na cópia, o item permanecerá na pilha como se nada tivesse acontecido.
O problema óbvio é que isso é simplesmente desajeitado e (para pessoas que não o usaram) inesperado.
O mesmo acontece em muitas outras partes do C ++: especialmente no código genérico, a segurança de exceção invade muitas partes do design - e isso se deve em grande parte ao fato de que pelo menos potencialmente envolve copiar objetos (que podem ser lançados), onde Java apenas criaria novas referências a objetos existentes (que não podem ser lançados, portanto, não precisamos nos preocupar com exceções).
Quanto a um script simples para inserir,
delete
quando necessário: se você pode determinar estaticamente quando excluir itens com base na estrutura do código-fonte, provavelmente não deveria estar usandonew
e,delete
em primeiro lugar.Deixe-me dar um exemplo de um programa para o qual isso quase certamente não seria possível: um sistema para fazer, acompanhar, cobrar (etc.) telefonemas. Quando você disca o telefone, ele cria um objeto de "chamada". O objeto de chamada controla quem você ligou, por quanto tempo você conversa com eles, etc., para adicionar registros apropriados aos logs de cobrança. O objeto de chamada monitora o status do hardware; portanto, quando você desliga, ele se destrói (usando o amplamente discutido
delete this;
). Só que não é tão trivial quanto "quando você desliga". Por exemplo, você pode iniciar uma chamada em conferência, conectar duas pessoas e desligar - mas a chamada continua entre essas duas partes mesmo depois de desligar (mas o faturamento pode mudar).fonte
Algo que acho que não foi mencionado aqui é que existem eficiências provenientes da coleta de lixo. Nos coletores Java mais usados, o principal local em que os objetos são alocados é uma área reservada para um coletor de cópias. Quando as coisas começam, esse espaço está vazio. À medida que os objetos são criados, eles são alocados um no lado do outro no grande espaço aberto até que ele não possa alocar um no espaço contíguo restante. O GC entra em ação e procura por objetos neste espaço que não estejam mortos. Ele copia os objetos ativos para outra área e os reúne (ou seja, sem fragmentação). O espaço antigo é considerado limpo. Em seguida, ele continua alocando objetos firmemente e repete esse processo, conforme necessário.
Existem dois benefícios nisso. A primeira é que não se gasta tempo excluindo os objetos não utilizados. Depois que os objetos ativos são copiados, a ardósia é considerada limpa e os objetos mortos são simplesmente esquecidos. Em muitas aplicações, a maioria dos objetos não dura muito tempo; portanto, o custo de copiar o conjunto ao vivo é barato comparado às economias obtidas por não ter que se preocupar com o conjunto morto.
O segundo benefício é que, quando um novo objeto é alocado, não há necessidade de procurar uma área contígua. A VM sempre sabe onde o próximo objeto será colocado (ressalva: simplificado ignorando a simultaneidade.)
Esse tipo de coleta e alocação é muito rápido. De uma perspectiva geral da taxa de transferência, é difícil vencer em muitos cenários. O problema é que alguns objetos permanecerão por mais tempo do que você deseja copiá-los e, finalmente, isso significa que o coletor pode precisar fazer uma pausa por um período significativo de vez em quando e quando isso acontecer pode ser imprevisível. Dependendo da duração da pausa e do tipo de aplicativo, isso pode ou não ser um problema. Há pelo menos um coletor sem pausa . Espero que exista uma troca de menor eficiência para obter a natureza sem pausa, mas uma das pessoas que fundou a empresa (Gil Tene) é um super especialista na GC e suas apresentações são uma excelente fonte de informações sobre a GC.
fonte
Na minha experiência pessoal em C ++ e até C, vazamentos de memória nunca foram um grande esforço a evitar. Com o procedimento de teste sensato e o Valgrind, por exemplo, qualquer vazamento físico causado por uma chamada
operator new/malloc
sem correspondênciadelete/free
é frequentemente detectado e corrigido rapidamente. Para ser justo, algumas grandes bases de códigos C ++ ou C ++ da velha escola podem ter alguns casos de borda obscuros que podem vazar fisicamente alguns bytes de memória aqui e ali, como resultado de nãodeleting/freeing
naquele caso de borda que voou sob o radar dos testes.No entanto, no que diz respeito às observações práticas, os aplicativos mais vazios que encontro (como os que consomem mais e mais memória quanto mais tempo você os executa, mesmo que a quantidade de dados com os quais trabalhamos não esteja aumentando) normalmente não são escritos em C ou C ++. Não encontro coisas como o Linux Kernel ou o Unreal Engine ou mesmo o código nativo usado para implementar o Java na lista de softwares com vazamentos que encontro.
O tipo mais proeminente de software com vazamento que costumo encontrar são coisas como miniaplicativos em Flash, como jogos em Flash, mesmo que eles usem coleta de lixo. E isso não é uma comparação justa se alguém deduzir algo disso, já que muitos aplicativos Flash são escritos por desenvolvedores iniciantes que provavelmente carecem de bons princípios de engenharia e procedimentos de teste (e da mesma forma, tenho certeza de que existem profissionais qualificados trabalhando com a GC que não lute contra software com vazamento), mas eu teria muito a dizer para quem pensa que o GC impede a gravação de software com vazamento.
Ponteiros pendurados
Agora, vindo do meu domínio específico, experiência e como um usando principalmente C e C ++ (e espero que os benefícios do GC variem dependendo de nossas experiências e necessidades), a coisa mais imediata que o GC resolve para mim não são problemas práticos de vazamento de memória, mas oscilando o acesso do ponteiro, e isso poderia literalmente ser um salva-vidas em cenários de missão crítica.
Infelizmente, em muitos casos em que o GC resolve o que seria um acesso de ponteiro danificado, ele substitui o mesmo tipo de erro do programador por um vazamento de memória lógica.
Se você imaginar esse jogo em Flash escrito por um codificador iniciante, ele pode armazenar referências a elementos do jogo em várias estruturas de dados, fazendo com que compartilhem a propriedade desses recursos do jogo. Infelizmente, digamos que ele cometa um erro ao esquecer de remover os elementos do jogo de uma das estruturas de dados ao avançar para o próximo estágio, impedindo que sejam liberados até que o jogo inteiro seja encerrado. No entanto, o jogo ainda parece funcionar bem porque os elementos não estão sendo desenhados ou afetam a interação do usuário. No entanto, o jogo está começando a usar cada vez mais memória, enquanto as taxas de quadros funcionam em uma apresentação de slides, enquanto o processamento oculto ainda circula por essa coleção oculta de elementos no jogo (que agora se tornou explosivo em tamanho). Esse é o tipo de problema que encontro com frequência nesses jogos em Flash.
Agora vamos dizer que o mesmo desenvolvedor iniciante escreveu o jogo em C ++. Nesse caso, normalmente haveria apenas uma estrutura central de dados no jogo que "possui" a memória enquanto outros apontam para essa memória. Se ele cometer o mesmo tipo de erro, é provável que, ao avançar para a próxima etapa, o jogo travar como resultado do acesso a indicadores pendentes (ou pior, fazer algo diferente de travar).
Esse é o tipo mais imediato de troca que eu costumo encontrar no meu domínio com mais frequência entre o GC e o GC. Na verdade, eu não ligo muito para o GC no meu domínio, o que não é muito crítico, porque as maiores lutas que já tive com software com vazamentos envolveram o uso aleatório do GC em uma equipe anterior, causando o tipo de vazamento descrito acima .
No meu domínio particular, na verdade, prefiro o software travando ou danificando-o em muitos casos, porque isso é pelo menos muito mais fácil de detectar do que tentar descobrir por que o software está misteriosamente consumindo quantidades explosivas de memória depois de executá-lo por meia hora enquanto todo o nosso os testes de unidade e integração passam sem nenhuma reclamação (nem mesmo da Valgrind, pois a memória está sendo liberada pelo GC após o desligamento). No entanto, isso não é um slam da GC da minha parte, nem uma tentativa de dizer que é inútil ou algo assim, mas não houve nenhum tipo de bala de prata, nem mesmo de perto, nas equipes com as quais trabalhei contra software com vazamento (para pelo contrário, tive a experiência oposta de que uma base de código utilizando o GC é a mais vazada que já encontrei). Para ser justo, muitos membros dessa equipe nem sabiam o que eram referências fracas,
Propriedade compartilhada e psicologia
O problema que encontro com a coleta de lixo que pode torná-lo tão propenso a "vazamentos de memória" (e insistirei em chamá-lo como tal, como o 'vazamento de espaço' se comporta exatamente da mesma maneira da perspectiva do usuário) nas mãos de aqueles que não o usam com cuidado se relacionam com "tendências humanas" até certo ponto em minha experiência. O problema com essa equipe e a base de código mais vazia que eu já encontrei foi que eles pareciam ter a impressão de que o GC permitiria que parassem de pensar em quem possui recursos.
No nosso caso, tínhamos tantos objetos referenciando um ao outro. Os modelos referenciam os materiais, juntamente com a biblioteca de materiais e o sistema de sombreamento. Os materiais referenciam as texturas junto com a biblioteca de texturas e certos shaders. As câmeras armazenariam referências a todos os tipos de entidades de cena que deveriam ser excluídas da renderização. A lista parecia continuar indefinidamente. Isso fez com que praticamente todos os recursos pesados do sistema pertencessem e estendessem sua vida útil em mais de 10 outros lugares no estado do aplicativo de uma só vez, e isso era muito, muito propenso a erros humanos de um tipo que se traduziriam em vazamentos (e não menor, estou falando de gigabytes em minutos com sérios problemas de usabilidade). Conceitualmente, todos esses recursos não precisavam ser compartilhados na propriedade, todos eles conceitualmente tinham um proprietário,
Se pararmos de pensar em quem possui qual memória e, felizmente, apenas armazenar referências que duram a vida toda a objetos em todo o lugar sem pensar nisso, o software não falhará como resultado de indicadores pendentes, mas quase certamente, sob esse mentalidade descuidada, comece a vazar memória como louca de maneiras muito difíceis de rastrear e que escapam a testes.
Se há um benefício prático para o ponteiro oscilante no meu domínio, é que ele causa falhas e travamentos muito desagradáveis. E isso tende a pelo menos incentivar os desenvolvedores, se eles querem enviar algo confiável, para começar a pensar sobre gerenciamento de recursos e fazer as coisas apropriadas necessárias para remover todas as referências / indicadores adicionais para um objeto que não é mais necessário conceitualmente.
Gerenciamento de recursos de aplicativos
Gerenciamento adequado de recursos é o nome do jogo, se estivermos falando de evitar vazamentos em aplicativos de longa duração, com estado persistente sendo armazenado, onde o vazamento pode causar sérios problemas de taxa de quadros e usabilidade. E gerenciar corretamente os recursos aqui não é menos difícil com ou sem GC. O trabalho não é menos manual para remover as referências apropriadas aos objetos que não são mais necessários, sejam eles ponteiros ou referências que prolongam a vida útil.
Esse é o desafio em meu domínio, não esquecendo o
delete
que fazemosnew
(a menos que falemos horas amadoras com testes, práticas e ferramentas de má qualidade). E isso requer reflexão e cuidado, se estamos usando o GC ou não.Multithreading
A outra questão que considero muito útil com o GC, se puder ser usada com muito cuidado em meu domínio, é simplificar o gerenciamento de recursos em contextos de multithreading. Se tomarmos cuidado para não armazenar referências que estendem a vida útil a recursos em mais de um local no estado do aplicativo, a natureza de extensão da vida útil das referências de GC pode ser extremamente útil como uma maneira de os segmentos estenderem temporariamente um recurso que está sendo acessado. sua vida útil por apenas uma curta duração, conforme necessário para o encadeamento concluir o processamento.
Eu acho que o uso muito cuidadoso do GC dessa maneira pode produzir um software muito correto, que não está vazando, enquanto simultaneamente simplifica o multithreading.
Existem maneiras de contornar isso, embora o GC esteja ausente. No meu caso, unificamos a representação da entidade da cena do software, com threads que temporariamente fazem com que os recursos da cena sejam estendidos por breves períodos de forma bastante generalizada antes de uma fase de limpeza. Isso pode parecer um pouco com o GC, mas a diferença é que não há "propriedade compartilhada" envolvida, apenas um design de processamento de cena uniforme em encadeamentos que adiam a destruição dos referidos recursos. Mesmo assim, seria muito mais simples confiar apenas no GC aqui se ele pudesse ser usado com muito cuidado com desenvolvedores conscientes, com cuidado para usar referências fracas nas áreas persistentes relevantes, para esses casos de multithreading.
C ++
Finalmente:
No C ++ moderno, isso geralmente não é algo que você deve fazer manualmente. Não se trata tanto de esquecer de fazê-lo. Quando você envolve o tratamento de exceções na imagem, mesmo que você tenha escrito uma
delete
chamada abaixo para alguma chamadanew
, algo pode aparecer no meio e nunca alcançá-ladelete
se você não confiar nas chamadas destruidoras automáticas inseridas pelo compilador para fazer isso por você.Com o C ++, você praticamente precisa, a menos que esteja trabalhando como um contexto incorporado, com exceções desativadas e bibliotecas especiais programadas deliberadamente para não serem lançadas, evite a limpeza manual de recursos (que inclui evitar chamadas manuais para desbloquear um mutex fora de um dtor , por exemplo, e não apenas desalocação de memória). O tratamento de exceções exige muito, portanto toda a limpeza de recursos deve ser automatizada através de destruidores em sua maior parte.
fonte