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()ehashCode(). - 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
newsã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.