Quais são as complexidades da programação não gerenciada por memória?


24

Ou, em outras palavras, quais problemas específicos a coleta automatizada de lixo resolveu? Como nunca fiz programação de baixo nível, não sei o quão complicado pode ser a liberação de recursos.

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?


3
Por favor, expandir-se para nos contar como a sua pergunta não foi respondida pelo artigo da Wikipedia sobre coleção garbace e mais especificamente a seção sobre os seus benefícios
yannis

Outro benefício é a segurança, por exemplo, a saturação de buffer é altamente explorável e muitas outras vulnerabilidades de segurança surgem do (mau) gerenciamento de memória.
StuperUser

7
@ SuperUser: Isso não tem nada a ver com a origem da memória. Você pode armazenar em buffer a memória excedida que veio de um GC. O fato de as linguagens do GC geralmente impedirem isso é ortogonal, e os idiomas menos de trinta anos atrás da tecnologia do GC estão sendo comparados para oferecer também proteção contra saturação de buffer.
DeadMG

Respostas:


29

Como nunca fiz programação de baixo nível, não sei o quão complicado pode ser a liberação de recursos.

Engraçado como a definição de "nível baixo" muda com o tempo. Quando eu estava aprendendo a programar, qualquer idioma que fornecesse um modelo de heap padronizado que possibilitasse um padrão simples de alocação / livre foi considerado de alto nível. Na programação de baixo nível , você precisa acompanhar a memória (não as alocações, mas as próprias localizações da memória!), Ou escrever seu próprio alocador de heap, se estiver realmente interessado.

Dito isto, não há realmente nada de assustador ou "complicado". Lembra quando você era criança e sua mãe lhe disse para guardar seus brinquedos quando terminar de brincar com eles, que ela não é sua criada e não ia limpar seu quarto para você? O gerenciamento de memória é simplesmente esse mesmo princípio aplicado ao código. (A GC é como ter uma empregada que irá limpar você, mas ela é muito preguiçosa e um pouco sem noção.) O princípio é simples: cada variável no seu código tem um e apenas um proprietário, e é responsabilidade desse proprietário libere a memória da variável quando ela não for mais necessária. ( O princípio da propriedade única) Isso requer uma chamada por alocação e existem vários esquemas que automatizam a propriedade e a limpeza de uma maneira ou de outra, para que você nem precise gravar essa chamada em seu próprio código.

A coleta de lixo deve resolver dois problemas. Invariavelmente, faz um trabalho muito ruim em um deles e, dependendo da implementação, pode ou não se dar bem com o outro. Os problemas são vazamentos de memória (mantendo a memória após o término) e referências pendentes (liberando memória antes do término). Vamos examinar os dois problemas:

Referências pendentes: Discutindo este primeiro porque é realmente sério. Você tem dois ponteiros para o mesmo objeto. Você libera um deles e não percebe o outro. Em algum momento posterior, você tenta ler (ou gravar ou liberar) o segundo. Um comportamento indefinido se segue. Se você não perceber, pode facilmente corromper sua memória. A coleta de lixo deve tornar esse problema impossível, garantindo que nada seja liberado até que todas as referências a ele desapareçam. Em uma linguagem totalmente gerenciada, isso quase funciona, até que você precise lidar com recursos de memória externos não gerenciados. Depois, volto à estaca 1. E em um idioma não gerenciado, as coisas ainda são mais complicadas. (Dê uma olhada no Mozilla '

Felizmente, lidar com esse problema é basicamente um problema resolvido. Você não precisa de um coletor de lixo, precisa de um gerenciador de memória de depuração. Eu uso o Delphi, por exemplo, e com uma única biblioteca externa e uma diretiva de compilador simples, posso definir o alocador como "Modo de depuração total". Isso adiciona uma sobrecarga de desempenho insignificante (menos de 5%) em troca da ativação de alguns recursos que controlam a memória usada. Se eu liberar um objeto, ele preenche sua memória com0x80bytes (facilmente reconhecíveis no depurador) e se eu tentar chamar um método virtual (incluindo o destruidor) em um objeto liberado, ele notará e interromperá o programa com uma caixa de erro com três rastreamentos de pilha - quando o objeto foi criado, quando foi liberado e onde estou agora - além de outras informações úteis, gera uma exceção. Obviamente, isso não é adequado para compilações de versões, mas torna trivial o rastreamento e a correção de problemas de referência pendentes.

O segundo problema são vazamentos de memória. É o que acontece quando você mantém a memória alocada quando não precisa mais dela. Isso pode acontecer em qualquer idioma, com ou sem coleta de lixo, e só pode ser corrigido escrevendo seu código corretamente. A coleta de lixo ajuda a atenuar uma forma específica de vazamento de memória, do tipo que ocorre quando você não tem referências válidas para um pedaço de memória que ainda não foi liberado, o que significa que a memória permanece alocada até o término do programa. Infelizmente, a única maneira de fazer isso de maneira automatizada é transformar cada alocação em um vazamento de memória!

Provavelmente vou ser enganado pelos defensores do GC se tentar dizer algo assim, então permita-me explicar. Lembre-se de que a definição de vazamento de memória está mantendo a memória alocada quando você não precisa mais dele. Além de não ter referências a algo, você também pode vazar memória tendo uma referência desnecessária, como mantê-la em um objeto contêiner quando deveria ter liberado. Vi alguns vazamentos de memória causados ​​por isso e são muito difíceis de rastrear se você possui um GC ou não, pois envolvem uma referência perfeitamente válida à memória e não há "bugs" claros para as ferramentas de depuração. pegar. Até onde eu sei, não há ferramenta automatizada que permita capturar esse tipo de vazamento de memória.

Portanto, um coletor de lixo se preocupa apenas com a variedade de vazamentos de memória sem referências, porque esse é o único tipo que pode ser tratado de maneira automatizada. Se pudesse assistir todas as suas referências a tudo e liberar todos os objetos assim que não houvesse nenhuma referência a ele, seria perfeito, pelo menos no que diz respeito ao problema de não referências. Fazer isso de maneira automatizada é chamado de contagem de referência e pode ser feito em algumas situações limitadas, mas ele tem seus próprios problemas para resolver. (Por exemplo, o objeto A que mantém uma referência ao objeto B, que mantém uma referência ao objeto A. Em um esquema de contagem de referências, nenhum objeto pode ser liberado automaticamente, mesmo quando não há referências externas para A ou B.) coletores de lixo usam rastreamentoem vez disso: comece com um conjunto de objetos em bom estado, encontre todos os objetos aos quais eles fazem referência, encontre todos os objetos aos quais eles fazem referência e assim por diante recursivamente até encontrar tudo. Tudo o que não é encontrado no processo de rastreamento é lixo e pode ser jogado fora. (Isso é bem-sucedido, é claro, requer uma linguagem gerenciada que impõe certas restrições ao sistema de tipos para garantir que o coletor de lixo de rastreamento sempre possa dizer a diferença entre uma referência e uma parte aleatória da memória que parece um ponteiro.)

Existem dois problemas com o rastreamento. Primeiro, é lento e, enquanto está acontecendo, o programa precisa ser mais ou menos pausado para evitar as condições da corrida. Isso pode causar problemas de execução perceptíveis quando o programa deve estar interagindo com um usuário ou desempenho atolado em um aplicativo de servidor. Isso pode ser mitigado por várias técnicas, como dividir a memória alocada em "gerações", com o princípio de que, se uma alocação não for coletada na primeira vez que você tentar, é provável que permaneça por um tempo. Tanto a estrutura .NET quanto a JVM usam coletores de lixo geracionais.

Infelizmente, isso alimenta o segundo problema: a memória não é liberada quando você termina. A menos que o rastreio seja executado imediatamente após o término de um objeto, ele permanecerá até o próximo rastreio, ou até mais, se passar da primeira geração. De fato, uma das melhores explicações do coletor de lixo .NET que eu já vi explica que, para tornar o processo o mais rápido possível, o GC deve adiar a coleta o máximo possível! Portanto, o problema de vazamento de memória é "resolvido" de maneira bizarra, vazando o máximo de memória possível pelo maior tempo possível! É isso que quero dizer quando digo que um GC transforma toda alocação em um vazamento de memória. Na verdade, não há garantia de que qualquer objeto dado vai sempre ser recolhidos.

Por que isso é um problema, quando a memória ainda é recuperada quando necessário? Por algumas razões. Primeiro, imagine alocar um objeto grande (um bitmap, por exemplo) que consome uma quantidade significativa de memória. E logo depois que você terminar, você precisará de outro objeto grande que ocupe a mesma quantidade (ou quase a mesma) de memória. Se o primeiro objeto tivesse sido liberado, o segundo poderia reutilizar sua memória. Porém, em um sistema coletado de lixo, é possível que você ainda esteja aguardando a execução do próximo rastreamento e, portanto, acaba desperdiçando desnecessariamente memória para um segundo objeto grande. É basicamente uma condição de corrida.

Segundo, manter a memória desnecessariamente, especialmente em grandes quantidades, pode causar problemas em um moderno sistema multitarefa. Se você consome muita memória física, isso pode fazer com que seu programa ou outros programas tenham que paginar (troque parte da memória deles para o disco), o que realmente atrasa as coisas. Para certos sistemas, como servidores, a paginação pode não apenas atrasar o sistema, mas também pode travar tudo se estiver sob carga.

Como o problema de referências pendentes, o problema de não referências pode ser resolvido com um gerenciador de memória de depuração. Mais uma vez, mencionarei o Modo de depuração completo do gerenciador de memória FastMM do Delphi, já que é o que eu estou mais familiarizado. (Tenho certeza de que existem sistemas semelhantes para outros idiomas.)

Quando um programa em execução no FastMM é finalizado, você pode opcionalmente informar a existência de todas as alocações que nunca foram liberadas. O Modo de depuração completo leva um passo adiante: ele pode salvar um arquivo em disco contendo não apenas o tipo de alocação, mas um rastreamento de pilha desde quando foi alocado e outras informações de depuração, para cada alocação vazada. Isso torna o rastreamento de vazamentos de memória sem referências trivial.

Quando você realmente olha para ele, a coleta de lixo pode ou não se dar bem com a prevenção de referências pendentes e, universalmente, faz um mau trabalho no tratamento de vazamentos de memória. Sua única virtude, de fato, não é a coleta de lixo em si, mas um efeito colateral: fornece uma maneira automatizada de executar a compactação de heap. Isso pode impedir um problema misterioso (esgotamento da memória através da fragmentação de heap) que pode matar programas que são executados continuamente por um longo tempo e têm um alto grau de perda de memória, e a compactação de heap é praticamente impossível sem a coleta de lixo. No entanto, atualmente, qualquer bom alocador de memória usa buckets para minimizar a fragmentação, o que significa que a fragmentação somente se torna realmente um problema em circunstâncias extremas. Para um programa em que a fragmentação de heap provavelmente seja um problema, é ' É aconselhável usar um coletor de lixo compacto. Mas, na IMO, em qualquer outro caso, o uso da coleta de lixo é uma otimização prematura, e existem melhores soluções para os problemas que ela "resolve".


5
Adoro essa resposta - continuo lendo de vez em quando. Não posso fazer uma observação relevante, então tudo o que posso dizer é - obrigado.
vemv

3
Gostaria de salientar que sim, os GCs tendem a "vazar" memória (pelo menos por um tempo), mas isso não é um problema, porque ele coletará a memória quando o alocador de memória não puder alocar memória antes da coleta. Com uma linguagem que não seja da GC, um vazamento sempre permanece um vazamento, o que significa que você realmente pode ficar sem memória devido a muita memória não coletada. "coleta de lixo é otimização prematura" ... O GC não é uma otimização e não foi projetado com isso em mente. Caso contrário, boa resposta.
Thomas Eding

7
@ Thomashoding: GC certamente é uma otimização; otimiza para o mínimo esforço do programador, à custa do desempenho e de várias outras métricas de qualidade do programa.
Mason Wheeler

5
Engraçado que você aponte para o rastreador de erros da Mozilla em um ponto, porque a Mozilla chegou a uma conclusão bem diferente. O Firefox tinha e continua a ter inúmeros problemas de segurança provenientes de erros de gerenciamento de memória. Observe que não se trata de quão fácil foi corrigir o erro uma vez detectado - geralmente o dano já está feito quando os desenvolvedores tomam conhecimento do problema. A Mozilla está financiando a linguagem de programação Rust precisamente para ajudar a impedir que esses erros sejam introduzidos em primeiro lugar.

11
Que a ferrugem não usar de coleta de lixo, porém, ele usa de contagem de referências exatamente como Mason está descrevendo, apenas com extensas verificações de tempo de compilação ao invés de ter que usar um depurador para detectar erros em tempo de execução ...
Sean Burton

13

Considerando uma técnica de gerenciamento de memória sem coleta de lixo de uma época equivalente, como os coletores de lixo em uso nos sistemas populares atuais, como o RAII do C ++. Dada essa abordagem, o custo de não usar a coleta automatizada de lixo é mínimo e o GC apresenta muitos de seus próprios problemas. Como tal, sugiro que "Não muito" seja a resposta para o seu problema.

Lembre-se, quando as pessoas pensam em não-GC, elas pensam malloce free. Mas essa é uma falácia lógica gigante - você compararia o gerenciamento de recursos não pertencentes à GC do início dos anos 70 aos coletores de lixo do final dos anos 90. Esta é obviamente uma comparação bastante injusta - os coletores de lixo que estavam em uso quando malloce freeforam projetados eram muito lentos para executar qualquer programa significativo, se bem me lembro. Comparar algo de um período vagamente equivalente, por exemplo unique_ptr, é muito mais significativo.

Os coletores de lixo podem lidar com os ciclos de referência mais facilmente, embora sejam experiências bastante raras. Além disso, os GCs podem simplesmente "vomitar" o código, porque o GC cuidará de todo o gerenciamento de memória, o que significa que eles podem levar a ciclos de desenvolvimento mais rápidos.

Por outro lado, eles tendem a ter problemas enormes ao lidar com a memória que veio de qualquer lugar, exceto seu próprio pool de GC. Além disso, eles perdem muito de seu benefício quando a concorrência está envolvida, porque você deve considerar a propriedade do objeto de qualquer maneira.

Edit: Muitas das coisas que você menciona não têm nada a ver com o GC. Você está confundindo gerenciamento de memória e orientação a objetos. Veja, o seguinte: se você programar em um sistema não gerenciado completo, como o C ++, poderá ter a verificação de limites que desejar, e as classes de contêineres padrão o oferecem. Não há nada de GC na verificação de limites, por exemplo, ou digitação forte.

Os problemas mencionados são resolvidos por orientação a objetos, não por GC. A origem da memória do array e garantir que você não escreva fora dele são conceitos ortogonais.

Edit: Vale a pena notar que técnicas mais avançadas podem evitar a necessidade de qualquer forma de alocação dinâmica de memória. Por exemplo, considere o uso disso , que implementa a combinação Y em C ++ sem nenhuma alocação dinâmica.


A discussão extensa aqui foi esclarecida: se todos puderem conversar com eles para discutir mais o assunto, eu realmente aprecio isso.

@DeadMG, você sabe o que o combinador deve fazer? É suposto combinar. Por definição, combinator é uma função sem variáveis ​​livres.
SK-logic

2
@ SK-logic: Eu poderia ter escolhido implementá-lo puramente por modelo e não ter nenhuma variável de membro. Mas você não seria capaz de aprovar fechamentos, o que limita significativamente sua utilidade. Gostaria de vir conversar?
DeadMG

@DeadMG, uma definição é clara. Sem variáveis ​​livres. Considero qualquer linguagem "funcional o suficiente" se for possível definir o combinador Y (corretamente, não do seu jeito). Um grande "+" é se é possível defini-lo via combinadores S, K e I. Caso contrário, a linguagem não será expressiva o suficiente.
SK-logic

4
@ SK-logic: Por que você não vem ao bate-papo , como o moderador perguntou? Além disso, um combinador Y é um combinador Y, ele faz o trabalho ou não. A versão Haskell do combinador Y é basicamente a mesma que esta, mas o estado expresso está oculto para você.
DeadMG

11

A "liberdade de se preocupar com a liberação de recursos" que as línguas coletadas pelo lixo supostamente fornecem é em grande parte uma ilusão. Continue adicionando itens em um mapa sem nunca remover nenhum, e você logo entenderá do que estou falando.

De fato, vazamentos de memória são bastante frequentes em programas escritos em linguagens GCed, porque essas linguagens tendem a tornar os programadores preguiçosos, e os fazem adquirir uma falsa sensação de segurança de que a linguagem sempre (de alguma forma mágica) cuidará de todos os objetos que eles não deseja mais pensar nisso.

A coleta de lixo é simplesmente um recurso necessário para linguagens que têm outro objetivo mais nobre: ​​tratar tudo como um ponteiro para um objeto e, ao mesmo tempo, esconder do programador o fato de ser um ponteiro, para que o programador não possa confirmar suicídio, tentando aritmética ponteiro e afins. Tudo o que é um objeto significa que as linguagens GCed precisam alocar objetos com muito mais frequência do que as linguagens que não são GCed, o que significa que se elas colocassem o ônus de desalocar esses objetos no programador, elas seriam imensamente pouco atraentes.

Além disso, a coleta de lixo é útil para fornecer ao programador a capacidade de escrever código restrito, manipular objetos dentro de expressões, de maneira funcional de programação, sem precisar dividir as expressões em instruções separadas para permitir a desalocação de todos os único objeto que participa da expressão.

Além de tudo isso, observe que, no começo da minha resposta, escrevi "é em grande parte uma ilusão". Eu não escrevi que é uma ilusão. Eu nem escrevi que é principalmente uma ilusão. A coleta de lixo é útil para tirar do programador a tarefa servil de cuidar da desalocação de seus objetos. Portanto, nesse sentido, é um recurso de produtividade.


4

O coletor de lixo não resolve nenhum "erro". É uma parte necessária de algumas semânticas de linguagens de alto nível. Com um GC, é possível definir níveis mais altos de abstrações, como fechamentos lexicais e similares, enquanto que com um gerenciamento manual de memória essas abstrações ficarão com vazamentos, vinculadas desnecessariamente aos níveis mais baixos de gerenciamento de recursos.

Um "princípio de propriedade única", mencionado nos comentários, é um bom exemplo de uma abstração tão vazada. Um desenvolvedor não deve se preocupar com o número de links para qualquer instância específica da estrutura de dados elementares; caso contrário, qualquer parte do código não seria genérica e transparente sem um grande número de limitações e requisitos adicionais (não visíveis diretamente no próprio código) . Esse código não pode ser composto por um código de nível superior, que é uma violação intolerável do princípio da separação de camadas de responsabilidade (um elemento essencial da engenharia de software, que infelizmente não é respeitado pela maioria dos desenvolvedores de nível inferior).


11
@Mason Wheeler, mesmo o C ++ implementa uma forma muito limitada de fechamentos. Mas não é quase um fechamento adequado e geralmente utilizável.
SK-logic

11
Você está errado. Nenhum GC pode protegê-lo do fato de que você não pode se referir a variáveis ​​de pilha. E é engraçado - em C ++, você também pode usar a abordagem "Copiar um ponteiro para uma variável alocada dinamicamente, que será destruída de maneira apropriada e automática".
DeadMG

11
@DeadMG, você não vê que seu código está vazando entidades de baixo nível através de qualquer outro nível que você constrói no topo?
SK-logic

11
@ SK-Logic: OK, temos um problema de terminologia. Qual é a sua definição de "fechamento real" e o que eles podem fazer que os fechamentos de Delphi não podem? (E incluindo qualquer coisa sobre o gerenciamento de memória em sua definição está se movendo os postes da baliza Vamos falar sobre o comportamento, nem detalhes de implementação..)
Mason Wheeler

11
@ SK-Logic: ... e você tem um exemplo de algo que pode ser feito com fechamentos simples sem tipo de lambda que os fechamentos da Delphi não podem realizar?
Mason Wheeler

2

Realmente, gerenciar sua própria memória é apenas mais uma fonte potencial de erros.

Se você esquecer uma chamada free(ou qualquer que seja o equivalente no idioma que você estiver usando), seu programa poderá passar em todos os testes, mas vazará memória. E em um programa moderadamente complexo, é muito fácil ignorar uma chamada free.


3
Perder freenão é a pior coisa. No início freeé muito mais devastador.
Herby

2
E o dobro free!
quant_dev

Ele Ele! Eu concordaria com os dois comentários acima. Eu nunca cometi uma dessas transgressões (tanto quanto sei), mas posso ver quão terríveis os efeitos podem ser. A resposta de quant_dev diz tudo - erros com alocação e desalocação de memória são notoriamente difíceis de encontrar e corrigir.
Dawood diz que restabelece Monica

11
Isso é uma falácia. Você está comparando "início de 1970" com "final de 1990". Os GCs que existiam no momento em que malloce freenão eram o caminho a seguir eram muito lentos para serem úteis para qualquer coisa. Você precisa compará-lo a uma abordagem moderna que não seja de GC, como o RAII.
DeadMG

2
@DeadMG RAII não é gerenciamento manual de memória
quant_dev

2

O recurso manual não é apenas tedioso, mas também difícil de depurar. Em outras palavras, não apenas é entediante acertar, mas também quando você erra, não é óbvio onde está o problema. Isso ocorre porque, diferentemente da divisão por zero, por exemplo, os efeitos do erro aparecem longe da fonte do erro, e conectar os pontos exige tempo, atenção e experiência.


1

Acho que a coleta de lixo recebe muito crédito por melhorias de idioma que nada têm a ver com o GC, além de fazer parte de uma grande onda de progresso.

O único benefício sólido para o GC que eu conheço é que você pode liberar um objeto em seu programa e saber que ele desaparecerá quando todos terminarem. Você pode passá-lo para o método de outra classe e não se preocupar com isso. Você não se importa com que outros métodos ele é passado ou com que outras classes o referenciam. (Vazamentos de memória são de responsabilidade da classe que faz referência a um objeto, não da classe que o criou.)

Sem o GC, é necessário rastrear todo o ciclo de vida da memória alocada. Toda vez que você passa um endereço para cima ou para baixo na sub-rotina que o criou, você tem uma referência descontrolada a essa memória. Nos velhos tempos, mesmo com apenas um encadeamento, a recursão e um sistema operacional básico (Windows NT) tornavam impossível controlar o acesso à memória alocada. Eu tive que montar o método livre em meu próprio sistema de alocação para manter os blocos de memória por um tempo até que todas as referências fossem esclarecidas. O tempo de espera era pura adivinhação, mas funcionou.

Portanto, esse é o único benefício que eu conheço, mas não poderia viver sem ele. Eu não acho que nenhum tipo de POO voe sem ele.


11
No topo da minha cabeça, Delphi e C ++ foram bem-sucedidos como linguagens OOP sem nenhum GC. Tudo o que você precisa para evitar "referências fora de controle" é um pouco de disciplina. Se você entende o Princípio da Propriedade Única, (veja minha resposta), os problemas dos quais você está falando aqui se tornam um total de não-problemas.
Mason Wheeler

@MasonWheeler: Quando é hora de liberar o objeto proprietário, ele precisa conhecer todos os lugares aos quais seus objetos de propriedade são referenciados. Manter essas informações e usá-las para remover as referências parece muito trabalho para mim. Muitas vezes descobri que as referências ainda não podiam ser esclarecidas. Eu tive que marcar o proprietário como excluído e trazê-lo à vida periodicamente para ver se ele poderia se libertar com segurança. Eu nunca usei o Delphi, mas por um pequeno sacrifício na eficiência da execução, o C # / Java me deu um grande impulso no tempo de desenvolvimento em relação ao C ++. (Nem todos devido à GC, mas ajudou.)
RalphChapin

1

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 freepara um determinado malloc. Definitivamente acontece.

No entanto, existem ferramentas como a valgrinddetecçã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 freenã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 freememorizamos 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.

insira a descrição da imagem aqui

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.

insira a descrição da imagem aqui

... 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).

insira a descrição da imagem aqui

... 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:

insira a descrição da imagem aqui

Esses links azuis não gerenciam a vida de Joe. Se queremos removê-lo da face da terra, solicitamos manualmente para destruí-lo, assim:

insira a descrição da imagem aqui

Agora, isso normalmente nos deixaria com ponteiros pendurados em todo o lugar, então vamos remover os ponteiros para Joe.

insira a descrição da imagem aqui

... 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.


-1

Aqui está uma lista de problemas enfrentados pelos programadores de C ++ ao lidar com memória:

  1. O problema de escopo ocorre na memória alocada pela pilha: sua vida útil não se estende para fora da função em que foi alocada. Existem três soluções principais para esse problema: memória heap e mover o ponto de alocação para cima na pilha de chamadas ou alocar a partir de objetos internos .
  2. O problema de Sizeof está na pilha alocada e alocada a partir do objeto interno e empilhar parcialmente a memória alocada: O tamanho do bloco de memória não pode mudar no tempo de execução. As soluções são matrizes de memória de pilha, ponteiros e bibliotecas e contêineres.
  3. Problema na ordem de definição é ao alocar a partir de objetos internos: as classes dentro do programa precisam estar na ordem correta. As soluções estão restringindo as dependências a uma árvore e reordenando as classes, sem usar declarações de encaminhamento e ponteiros e memória de pilha e usando declarações de encaminhamento.
  4. Problema de dentro para fora está na memória alocada ao objeto. O acesso à memória dentro dos objetos é dividido em duas partes, alguma memória está dentro de um objeto e outra memória está fora dele, e os programadores precisam escolher corretamente usar a composição ou as referências com base nessa decisão. As soluções estão tomando a decisão corretamente ou apontam e acumulam memória.
  5. Problema de objetos recursivos está na memória alocada a objetos. O tamanho dos objetos se torna infinito se o mesmo objeto for colocado dentro de si e as soluções são referências, memória de heap e ponteiros.
  6. O problema de rastreamento de propriedade está na memória alocada ao heap, o ponteiro que contém o endereço da memória alocada no heap deve ser passado do ponto de alocação para o ponto de desalocação. As soluções são recipientes de memória alocada por pilha, memória alocada a objeto, auto_ptr, shared_ptr, unique_ptr, stdlib.
  7. O problema da duplicação de propriedade está na memória alocada ao heap: a desalocação só pode ser feita uma vez. As soluções são memória alocada por pilha, memória alocada a objeto, auto_ptr, shared_ptr, unique_ptr, containers stdlib.
  8. O problema de ponteiro nulo está na memória alocada por heap: é permitido que os ponteiros sejam NULL, causando muitas falhas nas operações em tempo de execução. As soluções são memória de pilha, memória alocada a objetos e análise cuidadosa de áreas e referências de heap.
  9. O problema de vazamento de memória está na memória alocada por heap: Esquecendo de excluir a chamada para cada bloco de memória alocado. Soluções são ferramentas como o valgrind.
  10. O problema de estouro de pilha é para chamadas de função recursivas que estão usando memória de pilha. Normalmente, o tamanho da pilha é completamente determinado em tempo de compilação, exceto no caso de algoritmos recursivos. A definição incorreta do tamanho da pilha do sistema operacional também causa esse problema, pois não há como medir o tamanho necessário do espaço da pilha.

Como você pode ver, a memória heap está resolvendo muitos problemas existentes, mas causa complexidade adicional. O GC foi projetado para lidar com parte dessa complexidade. (desculpe se alguns nomes de problemas não são os nomes corretos para esses problemas - às vezes é difícil descobrir o nome correto)


11
-1: Não é uma resposta para a pergunta.
SJRJ12
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.