Pilha vs Heap

Quando um programa está em execução, os dados precisam ser armazenados na memória. O espaço ocupado e o local de armazenamento de cada dado são definidos pelo seu tipo. Durante a execução, duas áreas de memória são utilizadas para guardar informações: Pilha (Stack) e Heap (Monte).

Pilha (Stack)

A pilha é um bloco contínuo de memória que segue o princípio de estrutura de dados Último a entrar, primeiro a sair (LIFO). Ela armazena três categorias de dados:

  • Valores de variáveis de determinados tipos
  • Contexto atual de execução do programa
  • Parâmetros enviados para os métodos
A pilha se assemelha a um tubo de moedas: só é possível inserir e remover dados por uma única extremidade.

Todas as operações de memória da pilha são gerenciadas automaticamente pelo sistema, não há necessidade de manipulação manual por parte do desenvolvedor. Compreender a lógica interna da pilha simplifica a análise do fluxo de execução e a leitura da documentação oficial do C#.

Características fundamentais da pilha

  • Só é possível adicionar e remover dados no topo da pilha
  • Adicionar dados no topo: Push (empilhar)
  • Retirar dados do topo: Pop (desempilhar)

Propriedades da memória de pilha

  • Alocação e liberação de memória extremamente rápidas, utiliza blocos sequenciais de RAM
  • Ciclo de vida vinculado à chamada de método: o quadro de pilha é destruído automaticamente após o término da execução do método
  • Armazena: variáveis de tipo valor, parâmetros de método, variáveis locais, contexto de execução

Para entender melhor o funcionamento do heap, vamos analisar um exemplo de código simples

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Code language: JavaScript (javascript)

A lógica deste exemplo é muito simples. A partir da primeira linha da função Main — ponto de entrada do programa — criamos uma variável inteira a, depois a variável b na segunda linha. A terceira linha invoca a função Add, passando os valores de a e b para dentro do método. A variável x recebe o valor de a, y recebe o valor de b, e a função retorna o resultado da soma x + y igual a 30 para o ponto de chamada, onde o valor é atribuído à variável sum. Quando a função Add concluir sua execução, x e y são desempilhados, sum passa a valer 30 e o programa é finalizado após o término de Main. Abaixo, detalhamos passo a passo todo o processo de Push e Pop na pilha.

Fluxo de execução do programa e processo Push/Pop da pilha

Ao criar a variável a, os tipos primitivos nativos são alocados diretamente na pilha por meio de uma operação Push
Executamos a segunda linha de código, criando uma segunda variável inteira b com valor 20. Os dados empilhados primeiro ficam na base da pilha, os novos elementos ficam no topo
Na terceira linha, primeiro é executado Add(a,b) à direita do sinal de igual para invocar a função. Os valores de a e b são copiados para x e y: trata-se de passagem por valor, então a e x são variáveis independentes sem qualquer ligação entre si.
Nesta etapa apenas calculamos x + y e retornamos o resultado, sem armazenamento permanente na pilha. O valor 30 é enviado ao ponto de chamada como se fosse a instrução int sum = 30;. Após a execução de return x+y, a função Add se prepara para encerrar, todas as variáveis locais do método são processadas com Pop, liberando o espaço de memória que ocupavam para reutilização posterior.
A posição mais alta da pilha é chamada de topo (Top); após um Pop, o ponteiro do topo desce.
Ao finalizar Add, x e y são parâmetros locais desta função, sendo desempilhados um após o outro para liberar espaço e garantir o funcionamento fluido do sistema.
Após o Pop, x e y deixam de existir, e o valor 30 retornado por Add é enviado ao ponto de chamada para ser armazenado em sum.
A execução de int sum = 30 dispara uma nova operação Push: a variável sum com valor 30 é colocada no topo da pilha. Ao terminar essa linha, Main entra em estado de finalização e as variáveis locais sum, b, a serão destruídas sequencialmente via Pop.
As operações Pop sempre são executadas a partir do topo. A pilha é como um corredor com uma única entrada e saída: todos os elementos entram e saem pela mesma extremidade. Por isso a ordem de exclusão das variáveis em Main é inversa à ordem de criação: primeiro sum, depois b, por último a.
A variável b é desempilhada e destruída, o ponteiro do topo da pilha desce automaticamente.
Todas as variáveis foram desempilhadas, incluindo a última variável a. Rigorosamente falando, Main possui outra variável local: o vetor de strings args de Main(string[] args), responsável por receber os argumentos de linha de comando passados ao iniciar o console. Os dados deste vetor são armazenados em duas regiões distintas de memória, um tema relacionado ao heap que omitimos nesta demonstração para simplificar.

Lembre-se de um ponto essencial: durante a execução de uma função, as variáveis são criadas de cima para baixo e empilhadas na sequência a → b → entrada em Add → x → y. Ao terminar Add, x e y são desempilhados, depois sum é empilhado; quando Main encerra, as variáveis são removidas na ordem inversa da criação: sum, b, a.


Heap (Monte)

O heap é uma ampla área de memória destinada à alocação dinâmica de blocos para armazenar objetos de dados. Diferente da pilha, os dados do monte podem ser adicionados e removidos em qualquer ordem.

O programa não consegue excluir manualmente os objetos do heap; o Coletor de Lixo (GC) do CLR (Common Language Runtime) limpa automaticamente os objetos órfãos sem referências, que ficam inacessíveis. Isso evita erros causados pela liberação manual de memória, obrigatória em outras linguagens.

O heap pode ser comparado a uma mesa ampla, onde podemos colocar objetos livremente. Já a pilha, como explicado anteriormente, se assemelha a um tubo de moedas com apenas uma extremidade de acesso.

O heap é como uma mesa espaçosa sem restrições de disposição de objetos. O desenvolvedor não precisa apagar os objetos do monte manualmente, todo o gerenciamento fica a cargo do GC. Quando nenhuma parte do programa referencia mais um objeto, o GC o destrói periodicamente para liberar memória. Isso reduz muito a carga do programador e evita falhas no software causadas por vazamentos de memória não liberados. Linguagens como C++ exigem que o desenvolvedor libere toda a memória do heap manualmente.

Fluxo de coleta do GC

  1. O programa cria três objetos no heap e mantém referências válidas para eles
  2. O programa deixa de utilizar um dos objetos, todas as referências a ele desaparecem
  3. O GC detecta o objeto órfão sem referências e libera a memória que ele ocupa
  4. Coleta concluída, a memória liberada pode ser alocada para novos objetos

Propriedades da memória do heap

  • Espaços de memória descontínuos, velocidade de alocação inferior à da pilha
  • Ciclo de vida não limitado pelos limites do método, gerenciado de forma centralizada pelo GC
  • Armazena: instâncias de tipos de referência (classes, vetores, delegados etc.); na pilha só são guardados os endereços de memória dos objetos do heap

Vamos analisar um exemplo prático para tornar o conceito mais claro. Como mencionado antes, variáveis locais de tipo primitivo são armazenadas na pilha, enquanto tipos definidos pelo usuário geralmente residem no heap. Confira o código abaixo:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    //Variáveis de membro
    private string name;

    private int age;

    //Construtor, explicado mais adiante
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    //Método de membro
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Code language: JavaScript (javascript)

A ilustração abaixo mostra a estrutura de armazenamento em memória das quatro variáveis a, b, tom e lucy dentro de Main. Assim como na explicação da pilha, o código é executado de cima para baixo: primeiro a é empilhado, depois b. Os valores de a e b são salvos diretamente na pilha.

Variáveis de tipo primitivo são armazenadas diretamente na pilha
Os dados reais do tipo personalizado Student ficam armazenados no heap, na pilha só registramos o endereço do bloco de memória do objeto no monte.
Por exemplo, os dados de tom estão no bloco 00001 do heap, a pilha guarda o endereço 00001 para localizar os dados posteriormente.

Quando a função Main concluir sua execução, a referência de endereço lucy: 01010 presente na pilha é desempilhada e destruída. Apenas o endereço é apagado, os dados reais do objeto no heap permanecem intactos. Nesse momento, o objeto do monte perde todas as referências registradas pelas variáveis da pilha.

O objeto Lucy do bloco 01010 do heap não tem mais nenhuma variável na pilha que registre seu endereço, tornando-se um objeto órfão. O coletor de lixo GC o destruirá no momento adequado.
O objeto Lucy no heap perde todas as referências externas após o endereço armazenado na pilha ser desempilhado. O GC limpará a memória deste objeto a qualquer instante para reutilizar o espaço. Enquanto houver pelo menos uma referência ao objeto em alguma parte do programa, o GC não o destrói.

Áreas de memória

Pilha e heap são divisões lógicas de memória em nível de software, não partições físicas do hardware.

Pilha e heap são apenas separações lógicas definidas pelo software; a memória RAM física não possui módulos de hardware dedicados marcados como “área de pilha” ou “área de heap”.

A RAM física é composta apenas por chips uniformes com endereços físicos sequenciais, não existe nenhum marcador de hardware que diferencie pilha e heap

O sistema operacional aloca um espaço de memória virtual independente para cada programa em execução e divide previamente essa memória virtual em quatro segmentos lógicos principais:

Segmento de Código: Armazena as instruções compiladas do programa

Segmento Estático Global: Variáveis estáticas, constantes

Segmento de Pilha (Stack): A thread principal e as threads secundárias do programa possuem cada uma um bloco de pilha independente

Segmento de Heap (Monte): Grande pool de memória compartilhado para alocações dinâmicas

A pilha e o heap aqui descritos são apenas áreas lógicas atribuídas pelo SO a cada processo, sem isolamento em nível de hardware entre ambos.

Pilha vs Heap

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *