Estou tentando entender o processo real por trás das criações de objetos em Java - e suponho que outras linguagens de programação.
Seria errado supor que a inicialização do objeto em Java é a mesma de quando você usa malloc para uma estrutura em C?
Exemplo:
Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));
É por isso que se diz que os objetos estão na pilha e não na pilha? Porque eles são essencialmente apenas indicadores de dados?
scalar-replacement
) em campos simples que vivem apenas na pilha; mas isso é algo queJIT
faz, nãojavac
.Respostas:
Em C,
malloc()
aloca uma região de memória na pilha e retorna um ponteiro para ela. É tudo o que você recebe. A memória não é inicializada e você não tem garantia de que são todos zeros ou qualquer outra coisa.Em Java, a chamada
new
faz uma alocação baseada em heap da mesma formamalloc()
, mas você também recebe uma tonelada de conveniência adicional (ou sobrecarga, se preferir). Por exemplo, você não precisa especificar explicitamente o número de bytes a serem alocados. O compilador descobre isso para você com base no tipo de objeto que você está tentando alocar. Além disso, os construtores de objetos são chamados (para os quais você pode passar argumentos se quiser controlar como ocorre a inicialização). Quandonew
retorna, você garante um objeto inicializado.Mas sim, no final da chamada, o resultado
malloc()
enew
são simplesmente ponteiros para algum pedaço de dados baseados em heap.A segunda parte da sua pergunta pergunta sobre as diferenças entre uma pilha e uma pilha. Respostas muito mais abrangentes podem ser encontradas fazendo um curso (ou lendo um livro sobre) o design do compilador. Um curso sobre sistemas operacionais também seria útil. Também existem inúmeras perguntas e respostas sobre o SO sobre as pilhas e pilhas.
Dito isso, darei uma visão geral que, espero, não seja muito detalhada e tenha como objetivo explicar as diferenças em um nível bastante alto.
Fundamentalmente, o principal motivo para ter dois sistemas de gerenciamento de memória, ou seja, um heap e uma pilha, é a eficiência . Uma razão secundária é que cada um é melhor em certos tipos de problemas que o outro.
Pilhas são um pouco mais fáceis para eu entender como um conceito, então começo com pilhas. Vamos considerar esta função em C ...
O acima exposto parece bastante direto. Definimos uma função nomeada
add()
e passamos nos addends esquerdo e direito. A função os adiciona e retorna um resultado. Por favor, ignore todos os itens extremos, como estouros que possam ocorrer; neste momento, não é relevante para a discussão.O
add()
objetivo da função parece bastante direto, mas o que podemos dizer sobre seu ciclo de vida? Especialmente suas necessidades de utilização de memória?Mais importante ainda, o compilador sabe a priori (ou seja, em tempo de compilação) qual o tamanho dos tipos de dados e quantos serão usados. Os argumentos
lhs
erhs
sãosizeof(int)
, 4 bytes cada. A variávelresult
está, tambémsizeof(int)
. O compilador pode dizer que aadd()
função usa4 bytes * 3 ints
ou um total de 12 bytes de memória.Quando a
add()
função é chamada, um registro de hardware chamado ponteiro da pilha terá um endereço que aponta para o topo da pilha. Para alocar a memória que aadd()
função precisa executar, tudo o que o código de entrada da função precisa fazer é emitir uma única instrução de linguagem assembly para diminuir o valor do registro do ponteiro da pilha em 12. Ao fazer isso, ele cria armazenamento na pilha por trêsints
, um para cada umalhs
,rhs
eresult
. Obter o espaço de memória necessário ao executar uma única instrução é uma grande vitória em termos de velocidade, porque instruções únicas tendem a ser executadas em um relógio (1 bilionésimo de segundo de uma CPU de 1 GHz).Além disso, na visão do compilador, ele pode criar um mapa para as variáveis que se parecem muito com a indexação de uma matriz:
Novamente, tudo isso é muito rápido.
Quando a
add()
função sai, ela precisa ser limpa. Isso é feito subtraindo 12 bytes do registro do ponteiro da pilha. É semelhante a uma chamada,free()
mas usa apenas uma instrução da CPU e leva apenas um tique. É muito, muito rápido.Agora considere uma alocação baseada em heap. Isso entra em jogo quando não sabemos a priori quanta memória vamos precisar (ou seja, só aprenderemos sobre isso em tempo de execução).
Considere esta função:
Observe que a
addRandom()
função não sabe em tempo de compilação qual será o valor docount
argumento. Por isso, não faz sentido tentar definirarray
como definiríamos se estivéssemos colocando-o na pilha, assim:Se
count
for enorme, pode fazer com que nossa pilha cresça muito demais e substitua outros segmentos do programa. Quando esse estouro de pilha acontece, seu programa falha (ou pior).Portanto, nos casos em que não sabemos quanta memória precisaremos até o tempo de execução, usamos
malloc()
. Então, podemos apenas pedir o número de bytes que precisamos quando precisamos emalloc()
verificaremos se ele pode vender tantos bytes. Se puder, ótimo, recuperamos, se não, obtemos um ponteiro NULL que informa que a chamadamalloc()
falhou. Notavelmente, porém, o programa não falha! É claro que você, como programador, pode decidir que seu programa não poderá ser executado se a alocação de recursos falhar, mas o término iniciado pelo programador é diferente de uma falha espúria.Então agora temos que voltar para analisar a eficiência. O alocador de pilha é super rápido - uma instrução para alocar, uma instrução para desalocar e é feita pelo compilador, mas lembre-se de que a pilha é destinada a coisas como variáveis locais de tamanho conhecido, por isso tende a ser bastante pequena.
O alocador de heap, por outro lado, é várias ordens de magnitude mais lento. Ele precisa fazer uma pesquisa nas tabelas para verificar se há memória livre suficiente para poder vender a quantidade de memória que o usuário deseja. Ele precisa atualizar essas tabelas depois de vender a memória para garantir que ninguém mais possa usar esse bloco (essa contabilidade pode exigir que o alocador reserve memória para si , além do que planeja vender). O alocador deve empregar estratégias de bloqueio para garantir a venda da memória de maneira segura para threads. E quando a memória está finalmente
free()
d, que ocorre em momentos diferentes e normalmente em nenhuma ordem previsível, o alocador precisa encontrar blocos contíguos e costurá-los novamente para reparar a fragmentação de heap. Se parece que vai demorar mais do que uma única instrução de CPU para fazer tudo isso, você está certo! É muito complicado e leva um tempo.Mas montões são grandes. Muito maior que as pilhas. Podemos obter muita memória deles e eles são ótimos quando não sabemos em tempo de compilação quanta memória precisamos. Portanto, trocamos a velocidade por um sistema de memória gerenciada que nos recusa educadamente em vez de travar quando tentamos alocar algo muito grande.
Espero que ajude a responder algumas de suas perguntas. Entre em contato se desejar esclarecimentos sobre qualquer uma das opções acima.
fonte
int
não tem 8 bytes em uma plataforma de 64 bits. Ainda é 4. Junto com isso, é muito provável que o compilador otimize a terceiraint
saída da pilha no registro de retorno. De fato, é provável que os dois argumentos estejam registrados em qualquer plataforma de 64 bits.int
em plataformas de 64 bits. Você está correto queint
permanece em 4 bytes em Java. No entanto, deixei o restante da minha resposta, porque acredito que a otimização do compilador coloca o carrinho à frente do cavalo. Sim, você também está correto nesses pontos, mas a pergunta pede esclarecimentos sobre pilhas versus pilhas. O RVO, a passagem de argumentos por meio de registros, elisão de código etc. sobrecarrega os conceitos básicos e atrapalha a compreensão dos fundamentos.