Criação e Layout de Memória de Objetos em Java

  1. Instanciação de Objetos

1.1. Formas de Criação de Objetos

1. Uso do operador new

Esta é a abordagem mais frequente. Variantes incluem métodos estáticos de classes como Xxx ou fábricas como XxxBuilder/XxxFactory.

No JVM, ao executar new User(), ocorrem as seguintes etapas:

  1. Verifica se existe uma referência simbólica para a classe na área de constantes do metaspace e se a classe já foi carregada, resolvida e inicializada. Se não, o processo de carregamanto é acionado.
  2. Após o carregamento, aloca memória no heap para a instância, inicializa campos com valores padrão e, então, invoca o construtor para configuração dos campos. A ordem entre alocação e inicialização do construtor pode variar, pois a operação não é atômica.

2. Uso de reflexão

Reflexão permite criar instâncias dinamicamente. Dois métodos principais:

  • Class.newInstance(): Exige um construtor público sem parâmetros.
  • Constructor.newInstance(Object... args): Mais flexível, aceita construtores com parâmetros e acessibilidade restrita.

Exemplo de obtenção da classe e criação de instância:

// Obtendo a classe via reflexão
Class> classe = MinhaClasse.class;

// Criando instância com construtor padrão
Object instancia = classe.newInstance();

// Usando construtor com parâmetros
Constructor> construtor = classe.getConstructor(String.class, int.class);
Object instanciaComArgs = construtor.newInstance("teste", 42);

Um exemplo de classe para ilustrar:

public class Individuo {
    private String nome;
    private int idade;

    public Individuo() {}

    public Individuo(String nome, int idade) {
        this.nome = nome;
        this.idade = idade;
    }

    // Métodos getters e setters omitidos por brevidade
}

3. Uso de clone()

Clonagem cria uma cópia sem chamar construtores, requerendo implementação da interface Cloneable e do método clone(). A clonagem padrão é superficial.

public class ExemploClone implements Cloneable {
    private String texto;
    private Map<string string=""> dados;

    public ExemploClone(String texto, Map<string string=""> dados) {
        this.texto = texto;
        this.dados = dados;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
</string></string>

Ao clonar, referências a objetos como mapas são copiadas por referência, não por valor.

4. Uso de deserialização

Deserialização reconstrói objetos a partir de fluxos binários (arquivos, redes, etc.). Classes devem implementar Serializable.

public class Pessoa implements Serializable {
    int idade;
    int altura;
    String nome;

    public Pessoa(String nome, int idade, int altura) {
        this.nome = nome;
        this.idade = idade;
        this.altura = altura;
    }
}

Exemplo de serialização e deserialização:

// Serializando
try (ObjectOutputStream saida = new ObjectOutputStream(new FileOutputStream("dados.ser"))) {
    Pessoa pessoa = new Pessoa("Ana", 30, 165);
    saida.writeObject(pessoa);
}

// Deserializando
try (ObjectInputStream entrada = new ObjectInputStream(new FileInputStream("dados.ser"))) {
    Pessoa restaurada = (Pessoa) entrada.readObject();
}

5. Uso de bibliotecas externas

Certos cenários exigem instanciação sem construtores, como em serialização ou proxies. Classes como sun.misc.Unsafe oferecem métodos como allocateInstance() para isso.

Unsafe unsafe = Unsafe.getUnsafe(); // Obtenção simplificada
Object instancia = unsafe.allocateInstance(MinhaClasse.class);

1.2. Etapas de Criação de Objetos

1. Perspectiva do bytecode

Para Object obj = new Object(), o bytecode inclui:

  • new: Carrega a classe se necessário, aloca memória e inicializa campos com zero.
  • dup: Duplica a referência na pilha.
  • invokespecial: Invoca o construtor para inicialização personalizada.

2. Perspectiva da execução

  1. Verificação de classe: Confirma se a classe está carregada e resolvida no metaspace.

  2. Alocação de memória: Baseada na compactação do heap: usa colisão de ponteiro se contíguo ou lista livre se fragmentado.

  3. Concorrência: Mecanismos como CAS e TLAB garantem segurança durante alocações simultâneas.

  4. Inicialização de memória: Zera a memória alocada, exceto o cabeçalho do objeto.

  5. Configuração do cabeçalho: Preenche metadados como hashCode, informações de GC e lock.

  6. Execução do construtor: Inicializa campos conforme código definido, tornando o objeto utilizável.

  7. Layout de Memória de Objetos


2.1. Cabeçalho (Header)

O cabeçalho contém duas partes principais:

  • Dados de tempo de execução (mark word): Inclui hashCode, idade de GC, estado de lock, e identificadores de thread.
  • Ponteiro de tipo: Aponta para os metadados da classe no metaspace (InstanceKlass).
  • Para arrays, o cabeçalho também amrazena o comprimento.

2.2. Dados da Instância (Instance Data)

Armazena os valores reais dos campos definidos na classe e suas superclasses. Regras de layout:

  • Campos do mesmo tamanho são agrupados.
  • Campos de superclasses vêm antes dos subclasses.
  • Com CompactFields habilitado (padrão), campos de subclasses podem ser inseridos em espaços vazios.

2.3. Preenchimento de Alinhamento (Padding)

Opcional, usado para garantir que o objeto ocupe um número múltiplo de bytes, otimizando acesso à memória.

  1. Localização e Acesso a Objetos

3.1. Acesso por Handle

Uma área de handles no heap armazena ponteiros para instâncias e metadados. A referência (reference) aponta para o handle. Vantagem: referências permanecem estáveis mesmo se objetos forem movidos pelo coletor de lixo.

3.2. Acesso por Ponteiro Direto

A referência aponta diretamente para o objeto no heap. Vantagem: mais rápido, pois evita indireção extra.

3.3. Uso no HotSpot

No HotSpot JVM, referências apontam diretamente para objetos (instanceOopDesc). Os metadados de classe são acessdaos via ponteiro dentro do objeto, apontando para instanceKlass no metaspace.

Tags: java JVM Memória de Objetos Reflexão serialização

Publicado em 6-26 18:30