Heap x Stack

Wilson Santos
6 min readOct 1, 2018

--

Você ja olhou para os tipos primitivos do .net para ver como eles são construídos, por exemplo um int. Quando você fizer isso vai perceber que eles são structs e não classes no entanto se olhar para uma string percebera que se trata de uma classe.

tipo por valor
tipo por referencia

Mas você sabe por que existe essa diferença, quando devemos usar uma struct ou quando devemos usar uma classe? ou ainda qual a diferença de tipos por valor e tipos por referencia?

As Respostas para essas perguntas passam por entender como a memoria de nossos programas são alocadas e como elas são deslocadas, alem de conhecer esses carinhas Heap, Stack e o famigerado Garbage Collector ou gc para os íntimos.

Tipos por Valor:

Tipos por valor são tipos que ao serem copiados , geram um nova copia do valor de memoria, são alocados na memoria Stack e são liberado rapidamente normalmente ao sair do escopo da implementação.

Tipos por Referencia:

São tipos que ao serem copiados geram referencias , apontamentos usando os mesmos valores de memoria , são alocados no Heap porem os apontamentos ficam na Stack. são deslocados por mecanismo de limpeza como Garbage Collactor.

A memoria Heap (monte) :

A memoria do seu computador é um espaço de armazenamento físico ligado ao seu microprocessador , nela temos instruções e dados , essa mesma memoria é usado pelo sistema operacional e pelos programas que criamos e consumimos, os programas podem armazenar os dados em duas áreas principais , a Heap e a Stack. A Heap e um espaço maior destinado aos tipos por referencia normalmente classes , ela tem uma estrutura de dados mais flexível porem mais lenta.

A memoria Stack (pilha):

A memória Stack é um bloco de memória alocado para cada programa em runtime. Durante a vida de execução, quando uma função é invocada, todas as variáveis que ela utiliza são colocadas na Stack. Assim que a função é retornada ou o escopo de um bloco é finalizado, o mais breve possível os valores ali colocados serão descartados, liberando assim, a memória que estava sendo ocupada. Como falamos anteriormente, quando uma variável do tipo-valor é passada de uma função, uma cópia do valor é passado e, se esta função alterar este valor, não refletirá no valor original. Como os tipos primitivos dentro do .NET Framework são uma estrutura, eles são também armazenados na Stack.

Garbage Collector :

O GC é uma agente acionado pela CLR(Common Language Runtime) do framework responsável por liberar memoria alocada no Heap. Essa estratégica é muito assertiva pois livra o programador dessa responsabilidade árdua. Mas tudo tem um custo, o GC para poder garantir a integridade da suas analises precisa suspender o programa ou seja ele congela thread e isso gera problemas na performance da aplicação.

Bem os mecanismo que definem quando o GC vai ser executados são internos e muito complexos, isso não esta na mão do programador, não podemos definir quando memória será desalocada, mas podemos escolher com sabedoria quando ela será alocada.

Gerações do Garbage Collector

O GC usa uma série de heurísticas paras determinar quando ele deve varrer a heap e marcar quais objetos podem ser removidos, a questão é que para não varrer a heap todas as vezes que a coleta é acionada o GC classifica os objetos em 3 gerações 0,1,2.

As ultimas instancias que criamos os que estão dentro de blocos using, ou os que estão dentro de escopos curtos assim que eles saem de cena perdem o escopo eles são marcados para serem descartados na geração 0 , os que sobram vivos depois da coleta 0 são de Geração 1 e esses são liberados com menor frequência as de Geração 2 menos ainda.

Alem disso temos a questão de objetos grandes maiores que 85 mil bytes esses objetos ficam em uma área especial da heap e não são desalocados pelo fluxo normal de de coleta.

Então devemos tomar cuidado com objetos grandes que normalmente são string ou arrays , devemos avaliar a necessidade da criação de classes em detrimento de structs, e devemos pensar com cuidado nas estruturas que tem um tempo de vida mais longo pois essas estrutura vão passar por todas as gerações do GC gerando pressão sobre o mesmo.

GC em dois sabores

Podemos rodar o GC em modo workstation, que é o seu padrão e nele apenas uma thread será criada para gerencia a HEAP, ou podemos usar o modo server.

No modo Server o GC vai criar uma thread por núcleo de processamento e isso pode performar melhor em alguns casos.

Para fazer essa configuração basta colocar no arquivo de configuração da aplicação a chave gcServer enabled=”true”.

<configuration>
<runtime>
<gcServer enabled=”true” />
</runtime>
</configuration>

através de um programa que analise dump de memória o windbg podemos analisar como o GC se comporta.

Ao executar o comando !eeheap — gc podemos obter informações da gerações

E por fim chegamos na finalização

Existe um método especial chamado por muitos de destrutor, o finalize, é um método com o mesmo nome da classe, mas com um (~) antes.

Esse método é destinado a liberação de recursos não gerenciados, como leitura de Arquivos , Banco de dados e coisas do tipo.

No entanto isso pode ser extremante perigoso, pois classes com Finalize são promovidos para gerações mais antigas do GC, a nossa implementação pode dar erro durante a execução do finalize prejudicando a liberação de memoria, alem desses objetos com finalize serem mais lentos para serem alocados.

Minha sugestão é evitar esse método, a menos é claro que saibamos exatamente o que estamos fazendo. Um boa alternativa na minha opinião é implementar a interface IDisposable, esse Standart Constract do Framework .net fornece um método Dispose que pode ser usado para chamar os Respectivos “Disposes” de cada recurso não gerenciados da classe em questão, esse processo é conhecido como Cleanup.

Aliado ao uso de Dispose, podemos gerenciar nossos objetos através de ferramentas de Containers de Injeção de Dependência (DI) e de técnicas de inversão de controle(IOC).

Essas ferramentas normalmente se encarregam de chamar o método Dispose no fim do ciclo de vida de cada objeto.

Exemplo dessas ferramentas é o Sample Injector, mas no .net core ja temos nativamente esse recurso.

Como Funciona Boxing and Unboxing:

  1. Cliente cliente1 = new Cliente();
  2. Cliente cliente2 = cliente1;

O .net assim como outras linguagens modernas , possuem um sistema de tipos que permite que tudo seja tratado como objetos, tudo deriva de System.object, no entanto os tipos primitivos int, char, bool estão na STACK e os tipos complexos classes delegates arrays estão no HEAP, se tudo herda de Object a OOP diz que tipos mais abstratos devem receber tipos mais específicos, ou seja posso atribuir um int em um object. O trafego entre as áreas de memorias HEAP e STACK causado pelas conversões de tipos por valor e tipos por referencia são conhecidos o Boxing e o Unboxing.

Boxing

int p = 123
obejct box
box = p;

Unboxing

p=(int)box

Esse tipo de abordagem usa um tipo por valor(object) para alocar um valor de tipo primitivo causando desperdício de memoria alem de gerar itens de coleta já que o tipo object será alocado na heap.

Veja também

  1. O que é Span<T>
  2. Entendendo a Heap e o Garbage Collector em .NET
  3. Debug Profundo
  4. Como o Garbage Collector (GC) afeta a performance em .NET: Validação de CPF

--

--

Wilson Santos

Nos últimos 15 anos, venho desenvolvendo , aperfeiçoando e integrando sistemas, sou apaixonado por desenvolver e ensinar, nem tanto por escrever!.