Todo programa é um exagero, mas na prática eles usam esses dois modos de organizar a memória.
Na verdade, tem uma outra memória estática que já vai no executável e ele sendo carregado fica acessível do mesmo jeito, e é muito comum em strings. Por simplificação podemos aceitar essas duas que são as mais interessantes de estudar.
Tem ainda uma memória de controle do >NET que não vou abordar aqui que não parece ser o objetivo.
O que é armazenado na Stack?
Tipos de Valor (Value Types): Variáveis locais de tipos como int, double, bool, char, e structs são armazenadas diretamente na Stack.
Isso está errado, vou explicar aos poucos. Por hora, esses tipos podem ser alocadas na stack.
Parâmetros de Métodos: Os valores passados como argumentos para um método são colocados na Stack.
Simplificadamente podemos dizer que sim, lembrando que alguns desses valores são apenas referências para outros objetos, de forma explícita ou implícita. Parâmetros são variáveis locais como quaisquer outras, e estas "sempre" estão na pilha (por hora não vou falar de capturas de variáveis locais por closures).
Referências a Objetos: Quando você cria um objeto (um tipo de referência), o objeto em si vai para a Heap, mas a variável que aponta para ele (a referência/ponteiro) é armazenada na Stack.
Mais uma vez falso, existe sim a possibilidade de estar na stsck, mas é bem comum não estar.
Controle de Execução: A Stack gerencia qual método está em execução no momento. Cada chamada de método cria um "quadro" (stack frame) que contém suas variáveis locais e parâmetros. Quando o método termina, seu quadro é removido da pilha.
Isso é uma mistura de conceitos, existe a pilha de valores locais e existe a pilha de execução, que acaba contendo os valores também. Essa gerência não é tão simples de visualizar. Parece que tem algo complexo aí, mas é só uma mudança de um registrador específico. E o stack frame não necessariamente tem relação direta com o método. De forma geral o controle de execução não é armazenado na pilha, ainda que possa existir um mecanismo específico cuja implementação daquela versão possa desejar por algo na pilha, notadamente uma exceção.
Tamanho Fixo: A memória para um quadro de pilha é alocada no início da chamada do método
Isso não é uma absoluta verdade. Existe técnicas que podem fazer o tamanho variar em cada execução do método.
Gerenciamento Automático: A memória é liberada automaticamente
Só um pedantismo, a memória em si não é liberada, ele fica disponível para ser usada de novo por outro fenômeno que empilhe algo ali. A pulha como um todo nunca é liberada ou apagada, apenas tem o stack pointer se movendo para dizer onde deve empilhar a próxima alocação local.
Limitação de Tamanho: A Stack tem um tamanho limitado
É verdade mas isso pode ser configurado e às vezes abusado. Tem até mesmo tecnologias que usam 4GB de stack ou até mais (que é mais complicado0, mesmo não tendo alocação real de tudo isso, até seu uso efetivo (não vou entrar em detalhes de memória virtual). Um executável em Windows tinha até a última vez que vi o padrão de 1 MB de pilha, e acho que o .NET segue isso.
Estritamente falando o desenho da memória usado, apesar de eu não querer ser tão chato, seria:
+--------------------+
| 10 (4 bytes) |
+--------------------+
| 1 (1 byte) | <-- Topo da Stack
+--------------------+
Com a alinhamento o total dará 8 bytes.
A Heap é uma área de memória maior e mais flexível, usada para alocação dinâmica. É aqui que os objetos (instâncias de classes) residem.
Maior é um grande possibilidade, mas não uma obrigatoriedade. Contém diversos objetos, não necessariamente instâncias de classes.
O que é armazenado na Heap?
Instâncias de Tipos de Referência (Reference Types): Qualquer objeto criado com a palavra-chave new (como instâncias de class, arrays, string, delegates) é alocado na Heap.
E objetos de tipos por valor, sem falar nas próprias referências.
Velocidade: A alocação na Heap é mais lenta que na Stack, pois o sistema precisa encontrar um bloco de memória livre que seja grande o suficiente.
Isso não é uma verdade absoluta por causa do comportamento do GC do .NET, embora na prática provavelmente será um pouco pior na maioria das situações, mas nunca absurdamente como é em algumas outras tecnologias. O GC do .NET não precisa procurar um bloco livre de memória para armazenar, ele funciona de forma muito semelhante ao stack ou dispara uma coleta ou ainda pega o primeiro bloco que achar em uma parte específica do heap. É complicado explicar todos os detalhes aqui.
Gerenciamento pelo Garbage Collector (GC): A memória na Heap não é liberada automaticamente. O GC é um processo que roda em segundo plano, identifica objetos na Heap que não são mais referenciados por nenhuma variável na Stack e libera o espaço que eles ocupavam.
O GC não roda necessariamente em segundo plano, tem partes dele que não consegue isso, e tem implementações que pode ser diferente. Tem estratégias que o segundo plano por der pior e isso pode mudar de versão para versão. EU já conversei bastante com a Maoni Stephens sobre essas coisas, o GC é uma maravilha da engenharia e está sempre mudando.
A procura por referências à objetos começa nos registradores, passa pela pilha, muito eventualmente a memória estática, e o próprio heap que possuem pelo menos 4 gerações e tem otimizações para evitar ter que procurar por todo o heap, o que trás algumas desvantagens, e eu acho que deveria ser mais configurável isso. Claro que que para a maioria das aplicações é a melhor estratégia.
Não quero arrumar tudo isso, mas para ficar mais claro:
STACK HEAP
+-------------------------+ +-----------------------------------------+
| 0xA1B2 (4 ou 8 bytes) | | 101 (4 bytes) |
| 25 (4 bytes) | 0xA1B2 | Header do objeto (16 bytes tipicamente) |
+-------------------------+ +-----------------------------------------+
Leia mais em https://pt.stackoverflow.com/q/161846/101 e https://pt.stackoverflow.com/q/201284/101.
variável idade (tipo int) é um tipo de valor, então seu dado (25) é armazenado diretamente na Stack.
Reformulando: a variável idade é local, por acaso por valor e por isso está na pilha.
A variável aluno é uma referência. Ela também fica na Stack, mas seu valor não é o objeto em si, e sim o endereço (0xA1B2) onde o objeto Estudante foi alocado na Heap.
Reformulando: o valor da variável aluno é uma referência e como é uma variável local fica na pilha. Esse valor é o endereço onde está o objeto instanciado como uma classe (pio por referência)
Já falei dos itens errados que constam da tabela comparativa, mas tem erro também em:
Ciclo de Vida Curto (limitado ao escopo do método) Longo (até não ser mais referenciado)
Existem objeto de vida muito longa na pilha, especialmente no Main() e outros métodos que ficam no início da pilha. Existem objetos no heap que possuem vida extremamente curtas, menores que o tempo de execução de um método, ainda que eles possam não vir ser coletados nesse espaço de tempo, mas tem casos que é.
Curiosamente a segunda parte corrige erros da primeira parte do artigo o que mostra que foi feito com IA ou por alguém não muito atento, inclusive porque repete o que já havia sido dito antes e claramente está usando apenas outra referência nesse momento em vez de fazer um artigo coeso.
Não dá para afirmar que variável e valor são uma coisa só, até porque a variável é um nome que se dá a um endereço de memória. O que acontece em tipos por valor que é esse endereço já contém o objeto criado, portanto o valor dele está lá.
ela armazena um endereço de memória (uma referência ou ponteiro) que aponta para o local onde o objeto real está armazenado. Esse local é uma área da memória chamada Heap
Tecnicamente é possível referenciar um objeto que esteja na pilha, especialmente como parâmetros de métodos, não é obrigado estar no heap e é possível alocar alguns objetos por referência serem alocadas na pilha, sem contar que por otimização, ou seja, o programador não fica nem sabendo disso, vários objetos por referência são alocados na pilha, sempre que ele não encontrar impedimentos para fazer isso, e claro, isso depende de implementação, e a estão fazendo a otimização ficar mais agressiva em cada versão.
Heap: O objeto real, com todos os seus dados, é alocado na Heap
Nem sempre.
O GC começa na alocação, apesar do nome ele gerencia a memória dinâmica pelo começo, a coleta é apenas a parte final dele.
O GC costuma trabalhar quando ele percebe que falta memória em uma das gerações para fazer uma alocação, não é comum nas implementações atuais ele agir fora disso, ele não tem um temporizador ou outro mecanismo que escolhe o momento, apenas a falta de memória dá o gatilho e a estratégia exata depende de cada geração.
Embora colocar um valor null em todas as referências para um objeto realmente torna o objeto elegível para coleta, não quer dizer que ele já será coletado e nem que essa é uma boa estratégia para o programador usar, quase sempre isso é um erro e a perda da referência deveria acontecer de forma natural e não forçada. Se a pessoa não souber muito bem o que está fazendo, ela está usando uma gambiarra para resolver um problema que ela causou.
Veja mais em: https://www.tabnews.com.br/maniero/46130092-8b75-4048-9279-66de16662c45
Como eu sempre falo, a internet é cheia de erros, e a tendência por ela mesma e com a IA é pior, é sempre aumentar a quantidade de erros, até que ninguém mais aceite o certo já que "por maioria" o errado se tornal "oficial".
Eu fiz simplificações porque o assunto é complexo.
S2
Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).