Skip to main content

[.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 intstringstruct 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çãoarmazena 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

alt text

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.
  • 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;

alt text

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:

alt text

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.");

alt text

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.