Algumas questões que coloco abaixo foram resolvidas, outras não, mas são menos importantes.
Eu levei um susto quando vi o exemplo que estava comentando como BOM. Depois eu vi que explica que é o que o pessoal posta e de fato é uma solução pior em quase todos critérios, especialmente no gerenciamento de memória. O BOM faz zero alocações dinãmicas, e uma automática (o parâmetro) e algumas alocações estáticas. O RUIM faz como extra uma alocação dinâmica.
O artigo é interessante, parabéns por levantar a questão.
Mas tenho que corrigir o que está falho ou pode ser mal interpretado. Propositalmente não vou falar de todos os detalhes, o assunto é vasto.
Boa parte das linguagens não precisam ter conhecimento do tamanho do objeto para alocar na pilha. Java precisa, no momento, isso pode ser mudado. Se a intenção era falar especificamente de Java, está certo, mas não ficou claro. Todas as linguagens que alcançam um nível um pouco mais baixo, até mesmo C#, consegue alocar na stack sabendo o tamanho apenas no momento.
Quando fala em requisito e diz que "de preferência", não é requisito. E na verdade, não só não é um requisito como muito objeto na pilha tem um tempo de vida bastante longo, até mesmo por toda aplicação, especialmente na função de entrada. O requisito é sobre o objeto precisar sobreviver ao fim do escopo. Vou deixar links onde explico todos esses conceitos.
Em "na memória liberada" dito ali acho que queria dizer memória alocada.
Há uma confusão sobre tipos por valor serem liberados quando saem de escopo. Existem tipos por valor que são liberados pelo GC, afinal se um objeto está no heap e ele contém objetos por valor dentro dele, é o GC que o libera, não é sobre o escopo. Tipo por valor é diferente de objeto alocado na stack. Tipos por valores podem estar nas duas áreas.
Tipos por referência também podem estar nas duas áreas, não só no heap. Algumas linguagens limitam ao heap. Java não limita, mas favorece muito. Tanto não limita que tem implementações que o JITter é capaz de alocar uma classe, portanto um tipo por referência, na stack, como otimização. Isso não acontece muito, mas é possível. O programador não tem controle sobre isso. C#/.NET ainda não faz isso por gerar bem menos lixo e ter mais controle sobre a alocação de memória do que Java, mas nada impede de um dia ter.
Objetos alocados na stack são liberados quando sai do escopo. Objetos no heap duram até que o código ou o runtime (GC) mande liberar. Então eles podem até ter o tempo de vida definido de acordo com a execução, por isso chama-se memória dinâmica.
Como adendo, a memória das strings fixas, como as usadas nos códigos exemplo, não ficam nem no heap, nem na stack, ficam em memória estática e duram por toda a aplicação, a alocação é feita já na carga do executável.
Não sei se entendi bem o que quis dizer com "chamadas de função ocupam espaço na memória". Pelo que entendi, isso não é verdade. A chamada em si não ocupa espaço de memória algum (a não ser a memória para o código que faz a chamada, uma instrução), o que vai ocupar memória é a alocação dos objetos que estão no escopo da função chamada, e só assim pode acontecer o stack overflow, uma função que não aloca não tem esse problema. Pelo menos é assim em quase todas as linguagens.
Em geral os sistemas operacionais não impõem limite de tamanho para a stack de cada thread e todas podem ser configuradas durante a sua criação (a principal na carga). No Windows, até a última vez que eu vi, o padrão era 1MB, mas nada impede de criar com 4KB ou 4GB. É possível até ultrapassar esse limite.
Para alocar no heap, que costuma na maioria das linguagens, mas não necessariamente precisa ser compartilhado pelas threads, os objetos precisam ter tamanho conhecido no momento da alocação e não podem variar (sendo pedante). Seria fisicamente muito complicado dar variabilidade no tamanho do objeto. Pense em um caminhão onde os pacotes dentro dele podem mudar de tamanho, para onde vão os pacotes que estão do lado dele? É uma questão bem concreta. Por isso não pode encher até a boca caminhão tanque. E assim os motoristas roubam(vam) combustível, tirando uma parte e deixando o caminhão no sol para dar o mesmo volume (por isso hoje se mede a temperatura quando vai descarregar). Objetos na memória não são líquidos.
O tempo de vida do objeto no heap pode ser conhecido, o ideal até é que seja em tempo de compilação, e por sorte a maioria é. É mais complicado ainda quando só se conhece no momento da execução. Por isso o GC existe, ele facilita muito isso, e de forma segura. Quando há um GC não determinístico, caso da maioria das linguagens, então o tempo de vida importa menos, mas ainda importa. Em um GC determinístico, ou se sabe quando vai liberar a memória, ou tem um controle bem mais estrito de quando vai liberar (usa uma contagem de referência - algumas pessoas consideram esse tipo de GC como semi determinístico).
A única forma do tipo ter um tamanho determinado apenas no momento da alocação é ser uma coleção (um array). Em algumas linguagens é possível ter um array dentro de um outro tipo (uma classe por exemplo), o que fará que esse novo tipo tenha o tamanho variável em relação à sua criação, nunca depois. Pelo menos nunca vi linguagem que fugisse disso.
Alocar no heap é pior para a eficiência da execução e consumo de memória, sempre, até mesmo com gerenciamento manual de memória. Em alguns casos, ao contrário da crença popular, é possível ser pior fazendo manual do que com GC. O GC não pode ser sempre culpado pela ineficiência. Em muitos casos o GC piorará, e provavelmente gerará pausa de forma não determinística, que pode ser ruim em certos cenários.
O GC não manipula a tal da memória privada (não gosto do nome já que o heap também pode ser privado), ele só é necessário no heap se não quiser o gerenciamento manual. Na pilha o gerenciamento é sempre automático, por isso essa memória chama-se automática.
Existem GCs que pausam por tempo limitado, e o programador consegue minimizar isso também se ele souber usar corretamente todas essas coisas que estamos falando aqui.
Linguagens com GC são usadas em jogos de "tempo real", em locais de alta eficiência como o site Stack Overflow. Basta saber fazer. Nem todo mundo sabe, aí precisa achar outro tipo de solução. É tipo microsserviço, a pessoa não sabe fazer eficiente, então ela resolve quebrar em vários serviços para dar conta, ficando absurdamente mais caro e mais difícil de gerenciar e manter. O site SO não sofre com pausas, inclusive porque o GC avisa que vai coletar e pode tirar aquele nó do load balancer durante a pausa, que costuma ser de poucos milissegundos, nos piores casos, a maioria fica na casa dos poucos microssegundos.
Sem o GC não é de graça, ele não costuma dar pausas grandes, mas na soma total tem casos que consome mais tempo que o garbage collector.
Alocações no heap desnecessárias podem destruir a performance da aplicação. O GC pode agravar isso, mas nem sempre. Java e C# têm GCs muito modernos que são melhores em alguns cenários do que fazer em C, C++ ou Rust (ele aloca essencialmente no mesmo tempo que na stack que é muito rápida - só move um ponteiro, enquanto nas linguagens de mais baixo nível a alocação pode custar bem caro por ter que buscar um lugar para alocar). Claro que é possível fazer essas linguagens baterem C#, mas dá muito mais trabalho, você praticamente escreve um GC no seu código.
A explicação sobre as gerações do GC é sobre o .NET. Java é ligeiramente diferente, outras linguagens são bem diferentes, e não possuem gerações.
A Gen0 costuma levar microssegundos, a Gen1 não costuma passar de 1 milissegundo, e a Gen2 pode demorar bastante, mas a maior parte do tempo é feito em background e a pausa costuma ser bem pequena. As duas primeiras fazem uma cópia dos objetos sobreviventes para a próxima geração e a Gen2 faz o chamado mark & sweep (pelo menos algo muito parecido).
Todos os objetos precisam ser coletados pelo GC quando o heap é gerenciado por ele. Até existem linguagens que tem heap gerenciado e não gerenciado, mas é raro e não popular.
Chegando ao fim, quando a pessoa descobre o StringBuilder
ela tende a abusar dele e fazer o código ficar ineficiente quando ele não é a melhor opção.
Usar algo que tem tamanho suficiente ajuda porque se precisar aumentar o tamanho do objeto, não pode, então a solução é criar um outro objeto maior em outro lugar e copiar os dados para esse novo lugar, e depois o GC terá que coletar o objeto velho. Isso pode gerar uma progressão geométrica e te destruir.
Depois veremos se o problema no exemplo que eu achei é o mesmo que outros acharam.
Se você não seguir todos os links (em recursão) não vai aprender. Eu sei que dá trabalho, mas é assim que evolui. Eu percebo que as pessoas clicam só no primeiro link. Essa é a diferença de que aprende e que patina (os que já sabem disso tudo são os que mais vão clicar, tô certo kht? Foi uma total perda de tempo ler isso mesmo já sabendo?).
- https://pt.stackoverflow.com/q/3797/101
- https://pt.stackoverflow.com/q/121595/101
- https://pt.stackoverflow.com/q/56580/101
- https://pt.stackoverflow.com/q/95824/101
- https://pt.stackoverflow.com/q/161846/101
- https://pt.stackoverflow.com/q/106719/101
- https://pt.stackoverflow.com/q/135572/101
- https://pt.stackoverflow.com/q/255769/101
- https://pt.stackoverflow.com/q/200713/101
- https://pt.stackoverflow.com/q/205049/101
- https://pt.stackoverflow.com/q/344564/101
- https://pt.stackoverflow.com/q/110854/101
- https://pt.stackoverflow.com/q/422625/101
- https://pt.stackoverflow.com/q/191765/101
- https://pt.stackoverflow.com/q/581658/101
- https://pt.stackoverflow.com/q/278393/101 (leia os links dentro dela também, entenda o Shlemiel the painter's algorithm)
Faz sentido para você?
Espero ter ajudado.
Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente. Para saber quando, me segue nas suas plataformas preferidas. Quase não as uso, não terá infindas notificações (links aqui).