As inicializações de objetos em Java “Foo f = new Foo ()” são essencialmente iguais a usar malloc para um ponteiro em C?

9

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?

Jules
fonte
Os objetos são criados no heap para idiomas gerenciados como c # / java. No cpp, você pode criar objetos na pilha também
bas
Por que os criadores de Java / C # decidiram armazenar exclusivamente objetos no heap?
Jules
Eu penso por uma questão de simplicidade. Armazenar objetos na pilha e ultrapassá-los um nível mais profundo envolve copiar o objeto na pilha, o que envolve construtores de cópias. Eu não google para uma resposta correta, mas eu tenho certeza que você pode encontrar uma resposta mais satisfazendo a si mesmo (ou alguém vai elaborar sobre esta questão lado)
bas
Objetos @Jules em java ainda podem ser "descompactados" em tempo de execução (chamados scalar-replacement) em campos simples que vivem apenas na pilha; mas isso é algo que JITfaz, não javac.
Eugene
"Heap" é apenas um nome para um conjunto de propriedades associadas a objetos / memória alocados. No C / C ++, você pode selecionar entre dois conjuntos diferentes de propriedades, chamados “pilha” e “heap”, em C # e Java, todas as alocações de objetos têm o mesmo comportamento especificado, que se chama “heap”, que não é implica que essas propriedades são iguais às da pilha de C / C ++, na verdade elas não são. Isso não significa que as implementações não possam ter estratégias diferentes para gerenciar os objetos, implica que essas estratégias são irrelevantes para a lógica do aplicativo.
Holger

Respostas:

5

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 newfaz uma alocação baseada em heap da mesma forma malloc(), 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). Quando newretorna, você garante um objeto inicializado.

Mas sim, no final da chamada, o resultado malloc()e newsã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 ...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

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 lhse rhssão sizeof(int), 4 bytes cada. A variável resultestá, também sizeof(int). O compilador pode dizer que a add()função usa 4 bytes * 3 intsou 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 a add()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ês ints, um para cada uma lhs, rhse result. 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:

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

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:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

Observe que a addRandom()função não sabe em tempo de compilação qual será o valor do countargumento. Por isso, não faz sentido tentar definir arraycomo definiríamos se estivéssemos colocando-o na pilha, assim:

int array[count];

Se countfor 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 e malloc()verificaremos se ele pode vender tantos bytes. Se puder, ótimo, recuperamos, se não, obtemos um ponteiro NULL que informa que a chamada malloc()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á finalmentefree()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.

par
fonte
intnão tem 8 bytes em uma plataforma de 64 bits. Ainda é 4. Junto com isso, é muito provável que o compilador otimize a terceira intsaída da pilha no registro de retorno. De fato, é provável que os dois argumentos estejam registrados em qualquer plataforma de 64 bits.
SS Anne
Editei minha resposta para remover a declaração sobre 8 bytes intem plataformas de 64 bits. Você está correto que intpermanece 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.
par