Estrutura de Memória do JVM e Cópias de Objetos em Java

A estrutura de memória do JVM pode ser dividida em cinco componentes principais: área de métodos, heap, pilha da máquina virtual, contador de programa e pilha de métodos nativos.

1. Contador de Programa

O contador de programa armazena o endereço da próxima instrução de byte-code a ser executada. É exclusivo para cada thread e não gera problemas de estouro de memória.

2. Pilha da Máquina Virtual

  • Definição: A pilha contém quadros (frames) que representam chamadas de métodos. Cada thread possui sua própria pilha ao ser criada.
  • Características: O design baseado em pilha é uma das razões pelas quais o Java é cross-platform.
  • Ciclo de Vida: A vida útil da pilha está vinculada à do thread.
  • Dados Armazenados: Incluem tabelas de variáveis locais, pilhas de opernados, links dinâmicos e endereços de retorno.
  • Geração de Lixo: Não há coleta de lixo na pilha, pois os dados são descartados automaticamente após a execução dos métodos.
  • Estouro de Memória: Pode ocorrer com recursões mal controladas ou quadros excessivamente grandes.
  • Segurança de Threads: Variáveis locais são seguras desde que não escapem do escopo do método.

3. Pilha de Métodos Nativos

  • Definição: Usada para chamar métodos nativos implementados em linguagens como C/C++. Exemplos incluem clone() e hashCode().
  • Observação: Após chamar métodos nativos, essa memória fica fora do controle do JVM.
  • Comportamento: Ao chamar métodos nativos, o JVM mantém a pilha Java inalterada e conecta diretamente os métodos nativos.

4. Heap

  • Definição: Área compartilhada por todas as threads onde objetos instanciados com new são armazenados.
  • Coleta de Lixo: Foco principle da GC (Garbage Collector).
  • Subdivisões: Geração jovem (Eden, Survivor 0 e Survivor 1) e geração velha.

5. Área de Métodos (Metaspace)

  • Definição: Armazena informações de construção de classes, constantes, campos e métodos.
  • Pool de Constantes: Tabela usada pelo JVM para localizar classes, métodos e literais.
  • Localização: Antes do JDK 1.8, parte do heap; agora usa memória do sistema operacional.
  • Papel: Evita a criação repetida de objetos idênticos, economizando espaço e tempo.

6. Memória Direta

Utilizada principalmente para operações NIO, oferece alta performance de leitura/escrita, mas não é gerenciada diretamente pelo JVM.

Diferenças entre Heap e Pilha no Java

Característica Heap Pilha
Alocação Dinâmica, gerenciada pelo JVM Estática, gerenciada pelo compilador
Conteúdo Objetos e arrays Métodos, variáveis locais e parâmetros
Gestão GC automatizado Liberada automaticamente após execução do método
Tamanho Maior, expansível Menor, fixo
Velocidade Mais lenta Mais rápida

Cópias de Objetos em Java

1. Cópia Superficial

Uma cópia superficial replica apenas os valores primitivos e referências de um objeto, mantendo as referências originais intactas.


class Usuario implements Cloneable {
    private String nome;
    private Endereco endereco;

    public Usuario(String nome, Endereco endereco) {
        this.nome = nome;
        this.endereco = endereco;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Endereco {
    private String cidade;
    public Endereco(String cidade) {
        this.cidade = cidade;
    }
}

public class Teste {
    public static void main(String[] args) throws Exception {
        Endereco endereco = new Endereco("São Paulo");
        Usuario usuario1 = new Usuario("João", endereco);
        Usuario usuario2 = (Usuario) usuario1.clone();
        System.out.println(usuario1 == usuario2); // false
        System.out.println(usuario1.getEndereco() == usuario2.getEndereco()); // true
    }
}

2. Cópia Profunda

Uma cópia profunda cria réplicas independentes tanto dos valores primitivos quanto das referências internsa.


import java.io.*;

public class Teste {
    public static void main(String[] args) throws Exception {
        Endereco endereco = new Endereco("Rio de Janeiro");
        Usuario usuario1 = new Usuario("Maria", endereco);
        Usuario usuario2 = (Usuario) deepCopy(usuario1);
        System.out.println(usuario1 == usuario2); // false
        System.out.println(usuario1.getEndereco() == usuario2.getEndereco()); // false
    }

    private static Object deepCopy(Object obj) throws Exception {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(obj);
            try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
                 ObjectInputStream ois = new ObjectInputStream(bis)) {
                return ois.readObject();
            }
        }
    }
}

class Usuario implements Serializable {
    private String nome;
    private Endereco endereco;

    public Usuario(String nome, Endereco endereco) {
        this.nome = nome;
        this.endereco = endereco;
    }

    public Endereco getEndereco() {
        return endereco;
    }
}

class Endereco implements Serializable {
    private String cidade;

    public Endereco(String cidade) {
        this.cidade = cidade;
    }
}

Conclusão

Cópias superficiais e profundas são estratégias distintas para replicar objetos. Enquanto a primeira apenas clona valores primitivos e mantém referências compartilhadas, a segunda cria cópias completamente independentes, geralmente utilizando serialização.

Tags: JVM memory-management copying-objects

Publicado em 10-16 12:40