Entendendo um Pouco Mais Sobre Memória
Introdução
Quando comecei a programar, memória não era um assunto que eu considerava prioritário.
Mesmo após escrever meu primeiro programa na faculdade com C, onde fui introduzido aos conceitos da relação entre código e uso de memória, e acabei não dando muita atenção ao tema.
No entanto, a memória é uma parte importante do desenvolvimento, especialmente nas linguagens que utilizo atualmente, como JavaScript
e Python
. Ambas fazem uso de abstrações de gerenciamento de memória, como o algoritmo de Mark-and-Sweep.
Por isso, acredito que entender melhor esses conceitos pode me proporcionar mais autonomia e liberdade para compreender o que acontece "por trás dos panos".
Arquitetura de Von Neumann
Antes de abordar a alocação de memória, acho importante dar um passo atrás e entender como o computador e os programas realmente funcionam. Para isso, vou começar pela arquitetura de Von Neumann
Os computadores modernos se baseiam no conceito de Stored-Program, onde programas e dados são armazenados na mesma memória. Essa estrutura é composta por três unidades básicas:
- Unidade Central de Processamento (Central Processing Unit - CPU)
- Unidade de Memória (Memory Unit)
- Dispositivos de Input/Output (I/O)
De forma simplificada, a CPU é responsável pela execução das instruções dos programas. Seus componentes principais incluem:
- Unidade de Controle (Control Unit - CU) - Gerencia todos os sinais de entrada e saída do processador, direcionando o fluxo de I/O e controlando como os dados são movidos
- Unidade Aritmética e Lógica (Arithmetic and Logic Unit - ALU) - Realiza os cálculos aritméticos e operações lógicas que a CPU precisa executar
Entrando na Unidade de Memória (Memory Unit), existem ainda vários tipos que podem ser encontrados: Registradores , Memória Cache, Random Access Memory (RAM), Read-Only Memory (ROM), Memória Secundária (ou Armazenamento Permanente, como HDD e SSD), Memória Flash, ...
Agora, supondo que queira criar um arquivo index.js
, ele irá ser armazenado na Memória Secundária do computador. Quando eu executar o programa usando node index.js
, é usada a RAM para execussão de instruções e armazenamento temporário da variáveis em Registradores. Quando o programa termina de executar, esses recursos são liberados da memória e terminam sua execussão.
Alocação de Memória na Execussão de um Programa
Como comentei anteriormente, quando um programa é executado, o sistema aloca diferentes partes da memória para armazenar as diversas informações que o programa precisa para funcionar. Essas partes são: Texto, Dados, Stack e Heap.
O Texto é a primeira parte da memória e é nela que é mantida o conteúdo do programa. Esse texto refere-se ao código compilado (C
, Java
) ou interpretado (Javascript
e Python
).
Nos Dados estão as variáveis variáveis globais ou estáticas, que não mudam durante a execussão do programa.
No Stack, ou pilha de execussão, estão as pilhas de variáveis locais, parâmetros da função, estados de chamada das funções. Um detalhe nesse caso é que conforme funções são chamadas, novos frames são empilhados. Quando as funções terminam, esses frames são removidos da pilha.
Por fim, o Heap é utilizado para alocar memória dinâmica, ou seja, memória alocada durante a execussão do programa.
Para ilustrar os conceitos, criei o código em JavaScript
abaixo, que calcula o fatorial de um número usando recursão.
Nesse exemplo, o código do programa representa a seção de Texto. Como não estou utilizando variáveis globais ou estáticas, a seção de Dados não é utilizada. Quando chamo factorial(4)
, o programa começa a construir uma Pilha (Stack) de chamadas que cresce até que n
seja 0 ou 1. O Heap não é utilizado neste caso, pois a alocação dinâmica ocorre apenas para as variáveis locais de cada função.
// index.js
function factorial(n) {
if(n === 0 || n === 1) return 1;
return n * factorial(n - 1);
}
factorial(4);
O diagrama abaixo representa a sequência de chamadas recursivas que ocorrem na pilha durante a execução de factorial(4)
:
flowchart LR
s1["f(4)"] --> r1["4 * f(3)"]
s2["f(3)"] --> r2["3 * f(2)"]
s3["f(2)"] --> r3["2 * f(1)"]
s4["f(1)"] --> r4["1"]
Alocação de Memória (Heap) - Mark-and-Sweep
Enquanto o Stack lida com a memória de forma mais previsível e automática, o Heap requer um gerenciamento mais complexo. A memória alocada no Heap é dinâmica, o que significa que pode ser alocada e liberada durante a execução do programa. Para gerenciar essa memória dinâmica, o JavaScript
e Python
utilizam algoritmos de Coleta de Lixo - Garbage Collection, como o Mark-and-Sweep.
O Mark-and-Sweep é um algoritmo que gerencia a memória dinâmica, garantindo que os blocos de memória alocados, mas que não são mais necessários, sejam liberados para reutilização. Esse processo ocorre em duas fases principais:
-
Mark (Marcação) - Durante essa fase, o Garbage Collection percorre todos os objetos acessíveis a partir das referências conhecidas (por exemplo, variáveis globais e locais). Cada objeto acessível é marcado como "ativo". Objetos que não são acessíveis, ou seja, aqueles que não têm mais referências apontando para eles, não são marcados
-
Sweep (Varredura) - Após a fase de marcação, o Garbage Collection realiza uma varredura no Heap. Todos os objetos que não foram marcados como "ativos" são considerados "lixo" e têm sua memória liberada, tornando-a disponível para futuras alocações
// Pseudo algoritmo de varredura (Sweep)
Sweep()
For each object p in heap
If markedBit(p) = true then
markedBit(p) = false
else
heap.release(p)
Um exemplo bem simples disso são para objetos.
let user = {
name: "Matheus",
age: 26,
};
Ness exemplo, o objeto user
é alocado na Heap. Durante a execussão do programa, enquanto houver uma referência a user
, o Garbage Collection marcará este objeto como "ativo". Porém, se a referência de user
for removida:
user = null;
Agora, não há mais referências ao objeto original. Durante a próxima execução do Mark-and-Sweep, o coletor de lixo identificará que o objeto user
não está mais acessível e liberará a memória alocada para ele.
sequenceDiagram
Javascript Program ->> Heap Memory: Aloca objeto user: {name, age}
Javascript Program ->> Garbage Collector: Retém referênia para user
Garbage Collector ->> Heap Memory: Marca user como "ativo"
Javascript Program ->> Javascript Program: user = null
Garbage Collector ->> Heap Memory: Marca user como "inativo"
Garbage Collector ->> Heap Memory: Varre e libera memória
Conclusão
Essa parte mais conceitual sobre memória é bem interessante. Escrever sobre ela me ajudou a esclarecer alguns conceitos que ainda estavam confusos para mim.
Claro que ainda existe bem mais sobre o assunto. Fiquei bem surpreso assistindo o Vídeo do Daniel Ferreira sobre o assunto.
Por fim, espero ter agregado com o conteúdo.