[.NET Jogando no Very Hard] Gerenciamento de Memória Parte I
- [.NET Jogando no Very Hard] Gerenciamento de Memória Parte I
Definindo Expectativas
Antes de iniciarmos, seria interessante separarmos alguns termos conhecidos, como:
- Memória física: seria o hardware, aquele “pentezinho” DDR e/ou GDR que você já está acostumado.
- Memória virtual: um bloco lógico criado e gerenciado pelo Garbage Collector + Common Language Runtime.
Neste artigo abordaremos os temas relacionados a memória gerenciada em todas as suas etapas, destacando os seguintes tópicos:
- Quais os tipos de dados suportados pela CLR
- Como é o funcionamento obscuro do Garbage Collector, que será carinhosamente apelidado de GC
- Os diferente blocos de memória gerenciada (stack & heap) e seu funcionamento,
- Como analisar problemas de performance e resolve-los.
Tipos de Dados (Reference Types & Value Types)
Diferentemente dos *data types* com os quais estamos acostumados a escrever em nosso código como int, string, struct etc… a CLR encapsula estes dados em dois tipos macros, necessários para que o CLR + GC saibam como administra-los, são eles Reference Types(Tipo Referência) e Value Types(Tipo Valor)
Tipo Valor (Value Types)
Os tipos valores são aqueles que como próprio nome sugere contem o valor em si.
Esses tipos são armazenados na memória stack (detalharemos o que é stack e heap mais adiante).
Portanto no seguinte bloco de código o valor da variável XPTO será armazenado no bloco de memória stack
int xpto = 42;
Os seguintes data types são do tipo valor:
- int
- byte
- bool
- char
- decimal
- float
- double
- uint
- udecimal
- ufloat
- udouble
- struct
Uma struct em si pode ser uma pegadinha, pois ela pode conter algumas propriedade do tipo referência sendo essas armazenadas na memória heap.
A lista completa de você encontra aqui.
Tipo Referência (Reference Types)
Os tipos de referência, também conhecidos como tipos de instancia englobam todo o restante como:
- class
- interfaces
- delagate
- event
- string
- object
- dynamic
- arrays.
Estes tipos contem valores do tipo referência (ponteiro), ou seja eles contém o endereço da memória gerenciada, podem armazenar estruturas bastante complexas.
Os tipos referencia são armazenados na memória heap, que também vamos detalhar mais adiante.
Memória Gerenciada X Memória Não Gerenciada
Quando iniciamos a compilação de um projeto dotnet, a CLR já predefine um bloco de endereços desta memória gerenciada para o nosso futuro processo.
Aqui você deve estar se perguntando, porque você está falando em “memória gerenciada” existe “memória não gerenciada”?
A resposta é sim, e sua definição é bastante você encontra abaixo:
- Memória Gerenciada: trata-se do bloco de memória lógica gerenciado pelo Garbage Collector + CLR durante as etapas de compilação e execução.
- Memória Não Gerenciada é aquela disponibilizada pela kernel do sistema operacional disponível para alocação, quando como utilizamos malloc(), *calloc(), free(), realloc()* por exemplo.
Aqui realmente o entendimento fica complexo, e acredito que a melhor forma de lhe explicar isso seria entender oque acontece por debaixo do capo durante a execução de uma programa dotnet, e ao discorrer deste artigo ficará mais simples seu entendimento.
Memória Gerenciada Stack
Trata-se do bloco de memória gerenciada criado durante o tempo de compilação, armazena todas as variáveis locais do tipo valor.
Possui esse nome porque seu acesso é feito com o pattern LIFO(Last In First Out)
Na prática a CLR, através de alocações e liberações em pilha permite que esse bloco de memória se auto libere, variáveis, métodos são todos empilhados aqui, e a medida que o processo ocorre são executados os itens dessa pilha, essa memória é bastante curta e de extrema performance, e talvez você se pergunte “porque não armazenar tudo no bloco stack então”, aqui eu lhe farei uma provocação, pense no algoritmo abaixo:
var numbers = new Stack<long>(long.MaxValue);
for (long i = 0; i < long.MaxValue; i++)
{
numbers.Push(i);
}
numbers.Contains(85656585696);
Em quanto tempo esse algoritmo seria executado, O(n)?, O(n X n)?
Aqui mora uma confusão, por que quando estou falando em Stack, não estou me referindo a namespace System.Collections.Generic.Stack estou me referindo a um bloco escrito em código de maquina e ou IL(*Microsoft Intermediate Language **ressalvas aqui para compilação AOT)
Essa memória precisa ser curta para que seja efetiva e performática, na prática ela é apenas o guia de ordem de execução de nosso programa.
Memória Gerenciada Heap
Neste bloco de memória temos todas as alocações de instancia, ele é criado durante o tempo de execução de nosso programa.
Suas principais caraterísticas são o fato de seu tamanho ser virtualmente ilimitado, armazena todas as variáveis do tipo referência, não possui padrão de acesso, ou seja no caso de uma *lista linkada* podemos ter os seus respectivos valores espalhados em blocos que não estão virtualmente próximos, obviamente seu acesso é mais lento dado pela característica da aleatoriedade, é neste bloco aonde maioria dos dados manipulados pelo nosso código como: classes, interfaces, eventos etc… estão situados.
GC atua exclusivamente neste bloco, alocando e ou liberando memória, falaremos dele nas próximas partes desta série de artigos.
Entendendo a Alocação Stack & Heap
Agora faremos um passo a passo para facilitar nossa compreensão de como a alocação nos dois blocos gerenciados ocorre, vou tentar ser o mais simplista possível, mais quero que após esse estudo você imagine os projetos que já trabalhou neste cenários, perceba como somos meros consumidores de tecnologia.
Alocação Stack & Heap — Cenário 1
Veja o seguinte bloco de código:
var xpto = 42;
E abaixo oque de fato ocorre por de baixo do capo entre a CLR e GC
Aqui já temos vários pontos a observar
- Como a variável é do tipo valor seu nome e valor já foram diretamente alocados na memória stack.
- A CLR durante a execução de nosso programa já preparou tudo isso pra gente, a nomeação de nossa variável, a criação da pilha stack, o empilhamento em ordem das variáveis e etc…
Alocação Stack & Heap — Cenário 2
Neste cenário vamos observar uma atribuição nula, e como ela se comporta
bool? xpto = default;
Em nossa memória stack temos apenas o nome de sua variável sem valor algum, ou seja NULL neste ponto é algo como NADA.
Alocação Stack & Heap — Cenário 3
Neste cenário veremos a instanciação de um objeto complexo, primeiro vamos definir um classe Player.cs
namespace Game;
public class Player
{
public string? NickName { get; set; }
public double Score { get; set; }
}
Agora vamos criar uma instancia deste objeto ser possuir uma váriavel local:
new Player
{
NickName = "Alex",
Score = 100
}
Ou seja o nosso player é criado apenas em nossa memória heap, mas em nenhum momento ele possui atribuição o vinculo com a memória stack, em nossa memória teríamos o seguinte fato:

Como a classe possui uma string que também é do tipo referencia (ou seja por debaixo do capo trata-se de uma lista de char) vemos o relacionamento entre os objetos de forma clara, entre a instancia que criamos e string utilizada
Imagine isso com uma pilha de milhares de objetos, é algo como imagem abaixo demonstra
Nosso GC é responsável por encontrar cada item solicitado, para isso ele utiliza um conceito de lista/arvore de relacionamento.
Ele sabe onde cada um de nossos objetos está e com quem ele se relaciona, é simplesmente fantástico, vamos observar abaixo o cenário conclusivo, mais próximo de um programa real.
Alocação Stack & Heap — Cenário 4
Este cenário demonstra o relacionamento entre a memória Stack e Heap.
Vamos concretizar nosso entendimento ate então bastante obscuro para maioria dos desenvolvedores, vamos ao código.
using Game;
var alex = new Player
{
NickName = "Alex",
Score = 97
};
var amanda = new Player
{
NickName = "Amanda",
Score = 99
};
var winner = alex.Score > amanda.Score ? alex : amanda;
Console.WriteLine($"The winner is {winner.NickName} with {winner.Score} points.");

Aqui já temos bastante itens para observar:
1 A alocação das variáveis alex e amanda ocorre nos dois blocos de memória gerenciada, a declaração do nome e valor com endereço de memória(ponteiro) estão alocados na Stack, o valores do tipo referencia por sua vez estão armazenados na memória Heap.