Vazamento de memória - Memory leak

Na ciência da computação , um vazamento de memória é um tipo de vazamento de recurso que ocorre quando um programa de computador gerencia incorretamente as alocações de memória de uma forma que a memória que não é mais necessária não seja liberada. Um vazamento de memória também pode ocorrer quando um objeto é armazenado na memória, mas não pode ser acessado pelo código em execução. Um vazamento de memória tem sintomas semelhantes a uma série de outros problemas e geralmente só pode ser diagnosticado por um programador com acesso ao código-fonte do programa.

Um vazamento de espaço ocorre quando um programa de computador usa mais memória do que o necessário. Em contraste com os vazamentos de memória, onde a memória vazada nunca é liberada, a memória consumida por um vazamento de espaço é liberada, mas mais tarde do que o esperado.

Como podem esgotar a memória disponível do sistema à medida que um aplicativo é executado, os vazamentos de memória costumam ser a causa ou um fator que contribui para o envelhecimento do software .

Consequências

Um vazamento de memória reduz o desempenho do computador, reduzindo a quantidade de memória disponível. Eventualmente, na pior das hipóteses, muita memória disponível pode ser alocada e todo ou parte do sistema ou dispositivo para de funcionar corretamente, o aplicativo falha ou o sistema fica muito mais lento devido a batidas .

Os vazamentos de memória podem não ser sérios ou mesmo detectáveis ​​por meios normais. Em sistemas operacionais modernos, a memória normal usada por um aplicativo é liberada quando o aplicativo é encerrado. Isso significa que um vazamento de memória em um programa que é executado por um curto período de tempo pode não ser percebido e raramente é sério.

Vazamentos muito mais sérios incluem aqueles:

  • onde o programa é executado por um período prolongado e consome memória adicional ao longo do tempo, como tarefas em segundo plano em servidores, mas especialmente em dispositivos incorporados que podem permanecer em execução por muitos anos
  • onde a nova memória é alocada com frequência para tarefas únicas, como ao renderizar os quadros de um jogo de computador ou vídeo animado
  • onde o programa pode solicitar memória - como memória compartilhada  - que não é liberada, mesmo quando o programa termina
  • onde a memória é muito limitada, como em um sistema embarcado ou dispositivo portátil, ou onde o programa requer uma grande quantidade de memória para começar, deixando pouca margem para vazamento
  • onde o vazamento ocorre dentro do sistema operacional ou gerenciador de memória
  • quando um driver de dispositivo do sistema causa o vazamento
  • rodando em um sistema operacional que não libera memória automaticamente no encerramento do programa.

Um exemplo de vazamento de memória

O exemplo a seguir, escrito em pseudocódigo , tem como objetivo mostrar como pode ocorrer um vazamento de memória e seus efeitos, sem a necessidade de nenhum conhecimento de programação. O programa, neste caso, é parte de um software muito simples projetado para controlar um elevador . Esta parte do programa é executada sempre que alguém dentro do elevador pressiona o botão de um andar.

When a button is pressed:
  Get some memory, which will be used to remember the floor number
  Put the floor number into the memory
  Are we already on the target floor?
    If so, we have nothing to do: finished
    Otherwise:
      Wait until the lift is idle
      Go to the required floor
      Release the memory we used to remember the floor number

O vazamento de memória ocorreria se o número do andar solicitado for o mesmo andar em que o elevador está; a condição para liberar a memória seria ignorada. Cada vez que esse caso ocorre, mais memória é perdida.

Casos como esse geralmente não teriam efeitos imediatos. As pessoas não costumam apertar o botão do andar em que já estão e, em qualquer caso, o elevador pode ter memória sobressalente suficiente para que isso aconteça centenas ou milhares de vezes. No entanto, o elevador acabará por ficar sem memória. Isso pode levar meses ou anos, então pode não ser descoberto apesar de testes completos.

As consequências seriam desagradáveis; no mínimo, o elevador pararia de responder aos pedidos de mudança para outro andar (como quando é feita uma tentativa de chamar o elevador ou quando alguém está dentro e pressiona os botões do andar). Se outras partes do programa precisam de memória (uma parte atribuída para abrir e fechar a porta, por exemplo), então ninguém será capaz de entrar, e se alguém estiver dentro, eles ficarão presos (assumindo que as portas não podem ser aberto manualmente).

O vazamento de memória dura até que o sistema seja reiniciado. Por exemplo: se a energia do elevador fosse desligada ou em uma queda de energia, o programa parava de funcionar. Ao religar o programa, o programa seria reiniciado e toda a memória voltaria a ficar disponível, mas o lento processo de vazamento de memória reiniciaria junto com o programa, prejudicando o correto funcionamento do sistema.

O vazamento no exemplo acima pode ser corrigido trazendo a operação de 'liberação' para fora da condição:

When a button is pressed:
  Get some memory, which will be used to remember the floor number
  Put the floor number into the memory
  Are we already on the target floor?
    If not:
      Wait until the lift is idle
      Go to the required floor
  Release the memory we used to remember the floor number

Problemas de programação

Vazamentos de memória são um erro comum na programação, especialmente ao usar linguagens que não possuem coleta de lixo automática embutida , como C e C ++ . Normalmente, ocorre um vazamento de memória porque a memória alocada dinamicamente tornou-se inacessível . A prevalência de bugs de vazamento de memória levou ao desenvolvimento de uma série de ferramentas de depuração para detectar memória inacessível. BoundsChecker , Deleaker , IBM Rational Purify , Valgrind , Parasoft segurar ++ , Dr. Memória e memwatch são alguns dos mais populares depuradores de memória para programas em C e C ++. Os recursos de coleta de lixo "conservadores" podem ser adicionados a qualquer linguagem de programação que não possua um recurso integrado, e bibliotecas para fazer isso estão disponíveis para programas C e C ++. Um colecionador conservador encontra e recupera a maioria, mas não toda a memória inacessível.

Embora o gerenciador de memória possa recuperar memória inacessível, ele não pode liberar memória que ainda seja alcançável e, portanto, potencialmente ainda útil. Os gerenciadores de memória modernos, portanto, fornecem técnicas para os programadores marcarem semanticamente a memória com vários níveis de utilidade, que correspondem a vários níveis de acessibilidade . O gerenciador de memória não libera um objeto fortemente alcançável. Um objeto é fortemente alcançável se for alcançável diretamente por uma referência forte ou indiretamente por uma cadeia de referências fortes. (Uma referência forte é uma referência que, ao contrário de uma referência fraca , evita que um objeto seja coletado como lixo.) Para evitar isso, o desenvolvedor é responsável por limpar as referências após o uso, normalmente definindo a referência como nula quando ela não for mais necessário e, se necessário, cancelando o registro de quaisquer ouvintes de evento que mantenham referências fortes ao objeto.

Em geral, o gerenciamento automático de memória é mais robusto e conveniente para desenvolvedores, pois eles não precisam implementar rotinas de liberação ou se preocupar com a sequência em que a limpeza é realizada ou se um objeto ainda é referenciado ou não. É mais fácil para um programador saber quando uma referência não é mais necessária do que saber quando um objeto não é mais referenciado. No entanto, o gerenciamento automático de memória pode impor uma sobrecarga de desempenho e não elimina todos os erros de programação que causam vazamentos de memória.

RAII

RAII , abreviação de Resource Acquisition Is Initialization , é uma abordagem ao problema comumente usado em C ++ , D e Ada . Envolve a associação de objetos com escopo aos recursos adquiridos e a liberação automática dos recursos quando os objetos estão fora do escopo. Ao contrário da coleta de lixo, o RAII tem a vantagem de saber quando os objetos existem e quando não existem. Compare os seguintes exemplos de C e C ++:

/* C version */
#include <stdlib.h>

void f(int n)
{
  int* array = calloc(n, sizeof(int));
  do_some_work(array);
  free(array);
}
// C++ version
#include <vector>

void f(int n)
{
  std::vector<int> array (n);
  do_some_work(array);
}

A versão C, conforme implementada no exemplo, requer desalocação explícita; a matriz é alocada dinamicamente (do heap na maioria das implementações C) e continua a existir até que seja explicitamente liberada.

A versão C ++ não requer desalocação explícita; sempre ocorrerá automaticamente assim que o objeto arraysair do escopo, inclusive se uma exceção for lançada. Isso evita parte da sobrecarga dos esquemas de coleta de lixo . E, como os destruidores de objetos podem liberar outros recursos além da memória, o RAII ajuda a evitar o vazamento de recursos de entrada e saída acessados ​​por meio de um identificador , que a coleta de lixo de marcação e varredura não manipula normalmente. Isso inclui arquivos abertos, janelas abertas, notificações de usuário, objetos em uma biblioteca de desenho gráfico, primitivos de sincronização de thread, como seções críticas, conexões de rede e conexões com o Registro do Windows ou outro banco de dados.

No entanto, usar RAII corretamente nem sempre é fácil e tem suas próprias armadilhas. Por exemplo, se alguém não for cuidadoso, é possível criar ponteiros pendentes (ou referências) retornando dados por referência, apenas para que os dados sejam excluídos quando o objeto que os contém sai do escopo.

D usa uma combinação de RAII e coleta de lixo, empregando destruição automática quando está claro que um objeto não pode ser acessado fora de seu escopo original, e coleta de lixo caso contrário.

Contagem de referência e referências cíclicas

Os esquemas de coleta de lixo mais modernos costumam ser baseados em uma noção de acessibilidade - se você não tiver uma referência utilizável para a memória em questão, ela pode ser coletada. Outros esquemas de coleta de lixo podem ser baseados na contagem de referência , em que um objeto é responsável por manter o controle de quantas referências estão apontando para ele. Se o número cair para zero, espera-se que o objeto se libere e permita que sua memória seja recuperada. A falha desse modelo é que ele não lida com referências cíclicas, e é por isso que hoje em dia a maioria dos programadores está preparada para aceitar o fardo de sistemas de marcação e varredura mais caros .

O código do Visual Basic a seguir ilustra o vazamento de memória de contagem de referência canônica:

Dim A, B
Set A = CreateObject("Some.Thing")
Set B = CreateObject("Some.Thing")
' At this point, the two objects each have one reference,

Set A.member = B
Set B.member = A
' Now they each have two references.

Set A = Nothing   ' You could still get out of it...

Set B = Nothing   ' And now you've got a memory leak!

End

Na prática, esse exemplo trivial seria identificado imediatamente e corrigido. Na maioria dos exemplos reais, o ciclo de referências abrange mais de dois objetos e é mais difícil de detectar.

Um exemplo bem conhecido desse tipo de vazamento ganhou destaque com o surgimento das técnicas de programação AJAX em navegadores da web no problema do listener expirado . O código JavaScript que associou um elemento DOM a um manipulador de eventos e não conseguiu remover a referência antes de sair, vazaria memória (as páginas da web AJAX mantêm um determinado DOM ativo por muito mais tempo do que as páginas da web tradicionais, portanto, esse vazamento era muito mais aparente) .

Efeitos

Se um programa tiver um vazamento de memória e seu uso de memória estiver aumentando constantemente, geralmente não haverá um sintoma imediato. Cada sistema físico tem uma quantidade finita de memória e, se o vazamento de memória não for contido (por exemplo, reiniciando o programa que está vazando), ele acabará por causar problemas.

A maioria dos sistemas operacionais de desktop modernos possui memória principal, fisicamente alojada em microchips de RAM, e armazenamento secundário , como um disco rígido . A alocação de memória é dinâmica - cada processo obtém tanta memória quanto solicita. As páginas ativas são transferidas para a memória principal para acesso rápido; as páginas inativas são enviadas para o armazenamento secundário para liberar espaço, conforme necessário. Quando um único processo começa a consumir uma grande quantidade de memória, ele geralmente ocupa mais e mais da memória principal, empurrando outros programas para o armazenamento secundário - geralmente reduzindo significativamente o desempenho do sistema. Mesmo se o programa que está vazando for encerrado, pode levar algum tempo para que outros programas voltem para a memória principal e para que o desempenho volte ao normal.

Quando toda a memória em um sistema se esgota (se houver memória virtual ou apenas memória principal, como em um sistema incorporado), qualquer tentativa de alocar mais memória falhará. Isso geralmente faz com que o programa que tenta alocar a memória seja encerrado ou gere uma falha de segmentação . Alguns programas são projetados para se recuperar dessa situação (possivelmente recorrendo à memória pré-reservada). O primeiro programa a apresentar falta de memória pode ou não ser o programa que apresenta o vazamento de memória.

Alguns sistemas operacionais multitarefa têm mecanismos especiais para lidar com uma condição de falta de memória, como matar processos aleatoriamente (o que pode afetar processos "inocentes") ou matar o maior processo na memória (que provavelmente é o causador o problema). Alguns sistemas operacionais têm um limite de memória por processo, para evitar que qualquer programa monopolize toda a memória do sistema. A desvantagem desse arranjo é que o sistema operacional às vezes deve ser reconfigurado para permitir a operação adequada de programas que legitimamente requerem grandes quantidades de memória, como aqueles que lidam com gráficos, vídeo ou cálculos científicos.

O padrão "dente de serra" de utilização da memória: a queda repentina na memória usada é um sintoma candidato a um vazamento de memória.

Se o vazamento de memória estiver no kernel , o próprio sistema operacional provavelmente falhará. Os computadores sem gerenciamento sofisticado de memória, como sistemas incorporados, também podem falhar completamente devido a um vazamento de memória persistente.

Os sistemas publicamente acessíveis, como servidores da Web ou roteadores, estão sujeitos a ataques de negação de serviço se um invasor descobrir uma sequência de operações que pode desencadear um vazamento. Essa sequência é conhecida como exploit .

Um padrão "dente de serra" de utilização de memória pode ser um indicador de vazamento de memória em um aplicativo, principalmente se as quedas verticais coincidirem com reinicializações ou reinicializações desse aplicativo. Deve-se ter cuidado, porém, porque os pontos de coleta de lixo também podem causar esse padrão e mostrar um uso saudável do heap.

Outros consumidores de memória

Observe que o aumento constante do uso de memória não é necessariamente evidência de vazamento de memória. Alguns aplicativos armazenam quantidades cada vez maiores de informações na memória (por exemplo, como um cache ). Se o cache pode crescer tanto a ponto de causar problemas, isso pode ser um erro de programação ou design, mas não é um vazamento de memória, pois as informações permanecem nominalmente em uso. Em outros casos, os programas podem exigir uma quantidade excessivamente grande de memória porque o programador presumiu que a memória é sempre suficiente para uma tarefa específica; por exemplo, um processador de arquivo gráfico pode começar lendo todo o conteúdo de um arquivo de imagem e armazenando-o na memória, algo que não é viável quando uma imagem muito grande excede a memória disponível.

Em outras palavras, um vazamento de memória surge de um tipo específico de erro de programação e, sem acesso ao código do programa, alguém que percebe os sintomas pode apenas adivinhar que pode haver um vazamento de memória. Seria melhor usar termos como "aumento constante do uso da memória", onde esse conhecimento interno não existe.

Um exemplo simples em C

A seguinte função C vaza memória deliberadamente, perdendo o ponteiro para a memória alocada. Pode-se dizer que o vazamento ocorre assim que o ponteiro 'a' sai do escopo, ou seja, quando function_which_allocates () retorna sem liberar 'a'.

#include <stdlib.h>

void function_which_allocates(void) {
    /* allocate an array of 45 floats */
    float *a = malloc(sizeof(float) * 45);

    /* additional code making use of 'a' */

    /* return to main, having forgotten to free the memory we malloc'd */
}

int main(void) {
    function_which_allocates();

    /* the pointer 'a' no longer exists, and therefore cannot be freed,
     but the memory is still allocated. a leak has occurred. */
}

Veja também

Referências

links externos