Executando verificação de segurança...
11

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

Von Neumann Architecture

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.

Referências

Carregando publicação patrocinada...