Compreendendo o gerenciamento de memória em linguagens de programação modernas

Olá Habr! Apresento a você a tradução do artigo " Desmistificando o gerenciamento de memória nas linguagens de programação modernas ", de Deepu K Sasidharan.

Nesta série de artigos, gostaria de dissipar o véu do misticismo sobre o gerenciamento de memória em software (a seguir denominado software) e considerar em detalhes as possibilidades oferecidas pelas linguagens de programação modernas. Espero que meus artigos ajudem o leitor a olhar sob o capô dessas línguas e aprender algo novo para si.

Um estudo aprofundado dos conceitos de gerenciamento de memória permite que você escreva software mais eficiente, porque o estilo e a prática da codificação têm uma grande influência nos princípios de alocação de memória para as necessidades do programa.

Parte 1: Introdução ao gerenciamento de memória


O gerenciamento de memória é um conjunto de mecanismos que permitem controlar o acesso do programa à RAM do computador. Este tópico é muito importante no desenvolvimento de software e, ao mesmo tempo, causa dificuldades ou até permanece uma caixa preta para muitos programadores.

Para que serve a RAM?


Quando um programa é executado no sistema operacional de um computador, ele precisa acessar a memória de acesso aleatório (RAM) para:

  • faça upload de seu próprio bytecode para executar;
  • armazene os valores de variáveis ​​e estruturas de dados que são usadas no processo;
  • carregar módulos externos que o programa precisa para concluir tarefas.

Além do espaço usado para carregar seu próprio bytecode, o programa usa duas áreas de RAM ao trabalhar - a pilha (pilha) e a pilha (pilha).

Pilha


A pilha é usada para alocação estática de memória. Está organizado com base no princípio de "último a chegar, primeiro a sair" ( LIFO ). Você pode imaginar a pilha como uma pilha de livros - é permitido interagir apenas com o livro mais alto: leia-o ou coloque um novo.

  • graças ao princípio mencionado, a pilha permite executar rapidamente operações com dados - todas as manipulações são realizadas com o “livro principal da pilha”. O livro é adicionado ao topo se você precisar salvar dados ou retirado do topo se os dados precisarem ser lidos;
  • , , , — ;
  • — . , , — , . , , , . , , , — , ;
  • ;
  • ; ;
  • ;
  • (stack overflow), . , ;
  • , ;



JavaScript. , .


O heap é usado para alocar memória dinamicamente; no entanto, diferentemente da pilha, os dados no heap devem ser encontrados primeiro usando o "índice". Pode-se imaginar que uma pilha é uma biblioteca de vários níveis tão grande, na qual, seguindo determinadas instruções, você pode encontrar o livro necessário.

  • as operações no heap são executadas um pouco mais lentamente que na pilha, pois exigem uma etapa adicional para procurar dados;
  • heap armazena dados de tamanhos dinâmicos, por exemplo, uma lista na qual você pode adicionar um número arbitrário de elementos;
  • pilha comum a todos os threads de aplicativo;
  • devido à sua natureza dinâmica, o heap não é trivial no gerenciamento e, com ele, a maioria de todos os problemas e erros associados à memória. As formas de resolver esses problemas são fornecidas pelas linguagens de programação;
  • , — ( , ), , , ;
  • (out of memory), , ;
  • , , , .


?


Ao contrário dos discos rígidos, a RAM é muito limitada (embora os discos rígidos, é claro, também não sejam ilimitados). Se um programa consome memória sem liberá-la, ele absorve todas as reservas disponíveis e tenta ir além dos limites da memória. Depois, simplesmente cairá por conta própria ou, ainda mais dramático, derrubará o sistema operacional. Portanto, é altamente indesejável ser frívolo com manipulações de memória no desenvolvimento de software.

Abordagens diferentes


As linguagens de programação modernas tentam simplificar o trabalho com memória o máximo possível e remover parte da dor de cabeça dos desenvolvedores. Embora algumas linguagens veneráveis ​​ainda exijam controle manual, a maioria ainda oferece abordagens automáticas mais elegantes. Às vezes, uma linguagem usa várias abordagens para gerenciar a memória de uma só vez e, às vezes, um desenvolvedor pode até escolher qual opção será mais eficaz especificamente para suas tarefas (um bom exemplo é o C ++ ). Vamos passar para uma breve visão geral das várias abordagens.

Gerenciamento manual de memória


O idioma não fornece mecanismos para gerenciamento automático de memória. A alocação e liberação de memória para objetos criados permanece inteiramente na consciência do desenvolvedor. Um exemplo de tal linguagem a - a C . Ele fornece vários métodos ( malloc , realloc , calloc e free ) para gerenciar memória - o desenvolvedor deve usá-los para alocar e liberar memória em seu programa. Essa abordagem requer grande precisão e cuidado. Também é particularmente difícil para iniciantes.

Coletor de lixo


A coleta de lixo é o processo de gerenciamento automático de memória no heap, que consiste em encontrar partes não utilizadas da memória que estavam anteriormente ocupadas para as necessidades do programa. Essa é uma das opções mais populares para gerenciamento de memória nas linguagens de programação modernas. A rotina de coleta de lixo geralmente inicia em intervalos de tempo predeterminados e acontece que seu lançamento coincide com processos que consomem recursos, resultando em um atraso no aplicativo. JVM ( Java / Scala / Groovy / Kotlin ), JavaScript , Python , C # , Golang , OCaml eRuby são exemplos de idiomas populares que usam o coletor de lixo.

  • Coletor de lixo Mark & ​​Sweep: este é um algoritmo que opera em duas fases: primeiro, marca objetos na memória referenciados e libera memória de objetos que não foram marcados. Essa abordagem é usada, por exemplo, em JVM, C #, Ruby, JavaScript e Golang. Existem vários algoritmos diferentes de coleta de lixo para escolher na JVM, e os mecanismos JavaScript, como o V8, usam um algoritmo de identificação além da contagem de links. Esse coletor de lixo pode ser conectado em C e C ++ como uma biblioteca externa.

    Visualização do algoritmo de marcação: os objetos vinculados por links são marcados e os inacessíveis são excluídos
  • : — , . . , , , , PHP, Perl Python. C++;


(RAII)


RAII é um idioma de software no OOP, cujo significado é que a área de memória alocada para um objeto está estritamente ligada à sua vida útil. A memória é alocada no construtor e liberada no destruidor. Essa abordagem foi implementada pela primeira vez em C ++ e também é usada em Ada e Rust .

Contagem automática de links (ARC)


Essa abordagem é muito semelhante à coleta de lixo com contagem de referência, no entanto, em vez de iniciar o processo de contagem em determinados intervalos de tempo, as instruções para alocar e liberar memória são inseridas diretamente no bytecode no estágio de compilação. Quando o contador de referência chega a zero, a memória é liberada como parte do fluxo normal do programa.

A contagem automática de referências ainda não permite o processamento de links circulares e exige que o desenvolvedor use palavras-chave especiais para o processamento adicional de tais situações. O ARC é um dos recursos do tradutor Clang , portanto, está presente nas línguas Objective-C e Swift . Além disso, a contagem automática de referência está disponível para uso no Rust.e os novos padrões C ++ com ponteiros inteligentes .

Posse


Essa é uma combinação de RAII com o conceito de propriedade, quando cada valor na memória deve ter apenas uma variável de proprietário. Quando o proprietário sai da área de execução, a memória é liberada imediatamente. Podemos dizer que isso é aproximadamente como contar links na fase de compilação. Essa abordagem é usada no Rust e, ao mesmo tempo, não consegui encontrar nenhuma outra linguagem que usasse um mecanismo semelhante.




Este artigo examinou os conceitos básicos no campo do gerenciamento de memória. Cada linguagem de programação usa suas próprias implementações dessas abordagens e algoritmos otimizados para várias tarefas. Nas partes a seguir, examinaremos mais de perto as soluções de gerenciamento de memória em idiomas populares.

Leia também as outras partes da série:



Referências





Se você gostou do artigo, coloque um sinal de mais ou escreva um comentário.

Você pode assinar o autor do artigo no Twitter e no LinkedIn .

Ilustrações:
Visualização de pilhas feita usando o pythontutor .
Ilustração do conceito de propriedade: Link Clark, equipe Rust, sob licença Creative Commons Attribution Share-Alike v3.0 .

Pela prova de tradução, agradecimentos especiais a Alexander Maksimovsky e Katerina Shibakova

All Articles