Vazamentos físicos
O tipo de bugs que o GC aborda parece (pelo menos para um observador externo) o tipo de coisa que um programador que conhece bem sua linguagem, bibliotecas, conceitos, expressões idiomáticas, etc., não faria. Mas eu posso estar errado: o tratamento manual da memória é intrinsecamente complicado?
Vindo da extremidade C, que torna o gerenciamento de memória o mais manual e pronunciado possível, para compararmos extremos (o C ++ automatiza principalmente o gerenciamento de memória sem GC), eu diria "não realmente" no sentido de comparar com o GC quando trata de vazamentos . Um iniciante e às vezes até um profissional podem esquecer de escrever free
para um determinado malloc
. Definitivamente acontece.
No entanto, existem ferramentas como a valgrind
detecção de vazamentos que identificarão imediatamente, na execução do código, quando / onde esses erros ocorrerão até a linha exata do código. Quando isso é integrado ao IC, torna-se quase impossível mesclar esses erros e fácil como torta para corrigi-los. Portanto, nunca é grande coisa em nenhuma equipe / processo com padrões razoáveis.
Concedido, pode haver alguns casos exóticos de execução que voam sob o radar dos testes onde free
não foram chamados, talvez ao encontrar um erro de entrada externa obscuro como um arquivo corrompido, caso em que talvez o sistema vaze 32 bytes ou algo assim. Eu acho que isso definitivamente pode acontecer mesmo com bons padrões de teste e ferramentas de detecção de vazamento, mas também não seria tão crítico vazar um pouco de memória em algo que quase nunca acontece. Veremos um problema muito maior, onde podemos vazar recursos massivos, mesmo nos caminhos de execução comuns abaixo, de uma maneira que o GC não pode impedir.
Também é difícil sem algo parecido com uma pseudo-forma de GC (contagem de referência, por exemplo) quando o tempo de vida de um objeto precisa ser estendido para alguma forma de processamento adiado / assíncrono, talvez por outro encadeamento.
Ponteiros pendurados
O problema real com formas mais manuais de gerenciamento de memória não é vazamento para mim. Quantos aplicativos nativos escritos em C ou C ++ sabemos que são realmente vazados? O kernel do Linux está com vazamento? MySQL? CryEngine 3? Estações de trabalho e sintetizadores de áudio digital? O Java VM vaza (é implementado no código nativo)? Photoshop?
Acho que quando olhamos ao redor, os aplicativos mais vazios tendem a ser aqueles escritos usando esquemas de GC. Mas, antes que isso seja considerado um golpe na coleta de lixo, o código nativo tem um problema significativo que não está relacionado a vazamentos de memória.
A questão para mim sempre foi a segurança. Mesmo quando free
memorizamos através de um ponteiro, se houver outros ponteiros para o recurso, eles se tornarão ponteiros danificados (invalidados).
Quando tentamos acessar os pontos negativos desses ponteiros pendentes, acabamos tendo um comportamento indefinido, embora quase sempre uma violação de segfault / acesso leve a uma falha imediata e forte.
Todos os aplicativos nativos listados acima têm um ou dois casos obscuros que podem levar a uma falha principalmente devido a esse problema, e há definitivamente uma boa parte dos aplicativos de má qualidade, escritos em código nativo, que são muito pesados e frequentemente em grande parte devido a esse problema.
... e é porque o gerenciamento de recursos é difícil, independentemente de você usar o GC ou não. A diferença prática geralmente é vazar (GC) ou travar (sem GC) em face de um erro que leva à má administração de recursos.
Gerenciamento de recursos: coleta de lixo
O gerenciamento complexo de recursos é um processo manual difícil, não importa o quê. O GC não pode automatizar nada aqui.
Vamos dar um exemplo em que temos esse objeto, "Joe". Joe é referenciado por várias organizações das quais ele é membro. Todo mês, mais ou menos, eles extraem uma taxa de associação do cartão de crédito.
Também temos uma referência a Joe para controlar sua vida. Digamos que, como programadores, não precisamos mais do Joe. Ele está começando a nos incomodar e não precisamos mais dessas organizações que ele pertence para perder tempo lidando com ele. Então, tentamos limpá-lo da face da terra removendo sua referência da linha da vida.
... mas espere, estamos usando a coleta de lixo. Toda referência forte a Joe o manterá por perto. Portanto, também removemos referências a ele das organizações às quais ele pertence (cancelando a inscrição).
... exceto gritos, esquecemos de cancelar a assinatura de sua revista! Agora, Joe permanece na memória, incomodando-nos e consumindo recursos, e a empresa da revista também acaba continuando a processar a associação de Joe todos os meses.
Esse é o principal erro que pode fazer com que muitos programas complexos escritos usando esquemas de coleta de lixo vazem e comecem a usar mais e mais memória quanto mais tempo eles executam, e possivelmente mais e mais processamento (a assinatura periódica da revista). Eles esqueceram de remover uma ou mais dessas referências, impossibilitando que o coletor de lixo fizesse sua mágica até que todo o programa fosse desligado.
O programa não falha, no entanto. É perfeitamente seguro. Isso só vai manter a memória e Joe ainda vai demorar. Para muitas aplicações, esse tipo de comportamento com vazamento, no qual colocamos mais e mais memória / processamento em questão, pode ser muito preferível a uma falha grave, especialmente considerando a quantidade de memória e capacidade de processamento que nossas máquinas possuem atualmente.
Gerenciamento de Recursos: Manual
Agora vamos considerar a alternativa em que usamos ponteiros para Joe e o gerenciamento manual de memória, assim:
Esses links azuis não gerenciam a vida de Joe. Se queremos removê-lo da face da terra, solicitamos manualmente para destruí-lo, assim:
Agora, isso normalmente nos deixaria com ponteiros pendurados em todo o lugar, então vamos remover os ponteiros para Joe.
... gritos, cometemos o mesmo erro novamente e esquecemos de cancelar a assinatura da revista de Joe!
Exceto agora que temos um ponteiro pendente. Quando a assinatura da revista tenta processar a taxa mensal de Joe, o mundo inteiro vai explodir - normalmente temos o acidente instantâneo.
Esse mesmo erro básico de má administração de recursos, em que o desenvolvedor esqueceu de remover manualmente todos os ponteiros / referências a um recurso, pode levar a muitas falhas em aplicativos nativos. Eles não monopolizam a memória por mais tempo que executam normalmente, porque geralmente quebram completamente nesse caso.
Mundo real
Agora, o exemplo acima está usando um diagrama ridiculamente simples. Um aplicativo do mundo real pode exigir milhares de imagens unidas para cobrir um gráfico completo, com centenas de tipos diferentes de recursos armazenados em um gráfico de cena, recursos de GPU associados a alguns deles, aceleradores vinculados a outros, observadores distribuídos em centenas de plugins assistindo a vários tipos de entidades na cena em busca de mudanças, observadores observando observadores, áudios sincronizados com animações etc. Portanto, pode parecer fácil evitar o erro que descrevi acima, mas geralmente não é tão simples assim no mundo real base de código de produção para um aplicativo complexo que abrange milhões de linhas de código.
A chance de alguém, algum dia, administrar mal os recursos em algum lugar dessa base de código tende a ser bastante alta e essa probabilidade é a mesma com ou sem GC. A principal diferença é o que acontecerá como resultado desse erro, que também afeta potencialmente a rapidez com que esse erro será detectado e corrigido.
Crash vs. Leak
Agora qual é o pior? Um acidente imediato ou um vazamento silencioso de memória onde Joe simplesmente permanece misteriosamente?
A maioria pode responder ao último, mas digamos que este software foi projetado para funcionar por horas a fio, possivelmente dias, e cada um desses Joe e Jane que adicionamos aumenta o uso de memória do software em um gigabyte. Não é um software de missão crítica (falhas na verdade não matam usuários), mas um software de desempenho crítico.
Nesse caso, uma falha grave que aparece imediatamente durante a depuração, indicando o erro que você cometeu, pode ser preferível a apenas um software com vazamento que pode até voar sob o radar do seu procedimento de teste.
Por outro lado, se é um software de missão crítica em que o desempenho não é o objetivo, apenas não falha por nenhum meio possível, o vazamento pode ser realmente preferível.
Referências fracas
Existe uma espécie de híbrido dessas idéias disponíveis nos esquemas de GC conhecidos como referências fracas. Com referências fracas, podemos ter todas essas organizações com referência fraca a Joe, mas não impedir que ele seja removido quando a referência forte (proprietário / linha de vida de Joe) desaparecer. No entanto, temos o benefício de poder detectar quando Joe não está mais presente nessas referências fracas, o que nos permite obter um tipo de erro facilmente reproduzível.
Infelizmente, as referências fracas não são usadas tanto quanto provavelmente deveriam ser usadas; muitas vezes, aplicativos complexos de GC podem ser suscetíveis a vazamentos, mesmo que sejam potencialmente muito menos impactantes do que um aplicativo C complexo, por exemplo.
De qualquer forma, se o GC facilita ou dificulta sua vida depende de quão importante é para o seu software evitar vazamentos e se trata ou não de um gerenciamento complexo de recursos desse tipo.
No meu caso, trabalho em um campo crítico para o desempenho, onde os recursos abrangem centenas de megabytes a gigabytes, e não liberar essa memória quando os usuários solicitarem o descarregamento devido a um erro como o descrito acima pode ser menos preferível a uma falha. Os travamentos são fáceis de detectar e reproduzir, tornando-os frequentemente o tipo de bug favorito do programador, mesmo que seja o menos favorito do usuário, e muitas dessas falhas aparecem com um procedimento de teste sensato antes mesmo de chegarem ao usuário.
Enfim, essas são as diferenças entre o GC e o gerenciamento manual de memória. Para responder sua pergunta imediata, eu diria que o gerenciamento manual de memória é difícil, mas tem muito pouco a ver com vazamentos, e tanto o GC quanto as formas manuais de gerenciamento de memória ainda são muito difíceis quando o gerenciamento de recursos não é trivial. O GC sem dúvida tem um comportamento mais complicado aqui, onde o programa parece estar funcionando bem, mas consome cada vez mais recursos. O formulário manual é menos complicado, mas vai travar e queimar muito tempo com erros como o mostrado acima.