A máquina virtual Java (JVM) gerencia diversas áreas de memória em tempo de execução. Estas áreas são divididas entre regiões compartilhadas entre threads e regiões privadas para cada thread.
Áreas de Dados em Tempo de Execução
As principais regiões incluem o contador de programa, a pilha de máquina virtual, a pilha de métodos nativos, o heap e a área de métodos.
Contador de Programa
Este é um pequeno espaço que atua como um indicador do endereço da instrução bytecode atual sendo executada pela thread. Para cada thread, um contador de programa independente é mantido, permitindo que a execução retome corretamente após alternâncias de thread. Esta é a única região da JVM onde não é especificada uma condição de erro OutOfMemoryError, pois é gerenciada internamente.
Pilha de Máquina Virtual
Cada thread possui sua própria pilha, criada com a thread e com o mesmo ciclo de vida. A pilha contém quadros de pilha para cada método invocado, que armazenam a tabela de variáveis locais, a pilha de operandos, links dinâmicos e o endereço de retorno.
- A tabela de variáveis locais armazena tipos de dados primitivos e referências a objetos. Tipos como long e double (64 bits) ocupam dois espaços de variáveis, enquanto outros tipos ocupam um único espaço.
- O tamanho da tabela de variáveis locais é determinado em tempo de compilação e não muda durante a execução do método.
Uma pilha pode ter tamanho fixo ou dinâmico. Com tamanho fixo, um excesso de chamadas de método pode causar um erro StackOverflowError. Se a pilha puder se expandir dinamicamente, uma falha ao alocar memória adicional resultará em um erro OutOfMemoryError.
Exemplo de código que simula um estouro de pilha:
public class StackSimulation {
public static void main(String[] args) {
new StackSimulation().callRecursive();
}
private void callRecursive() {
System.out.println("Executando recursão...");
callRecursive();
}
}
Ao executar, este código produz um erro similar a:
Exception in thread "main" java.lang.StackOverflowError
at java.io.PrintStream.write(PrintStream.java:526)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
Pilha de Métodos Nativos
Semelhante à pilha de máquina virtual, mas dedicada à execução de métodos nativos. Na impleemntação HotSpot da JVM, ambas as pilhas são frequentemente unificadas. Os mesmos tipos de erro (StackOverflowError e OutOfMemoryError) podem ocorrer.
Heap
Esta é a maior área de memória gerenciada pela JVM, onde as instâncias dos objetos são alocadas. É uma região compartilhada entre threads e o principle alvo do coletor de lixo (GC).
O heap é tipicamente dividido em gerações para otimização da coleta de lixo:
- Geração jovem (incluindo espaços Eden e de sobrevivência)
- Geração antiga (para objetos de longa duração)
Para gerenciar a alocação de memória de forma segura entre threads, o heap pode conter buffers de alocação local de thread (TLABs). A memória do heap pode ser fisicamente descontígua, mas deve ser contígua logicamente. Seu tamanho é configurável via parâmetros como -Xms e -Xmx, e a insuficiência de memória leva a um erro OutOfMemoryError.
Área de Métodos
Esta região compartilhada armazena dados carregados pela JVM, como a estrutura das classes, constantes, variáveis estáticas e código compilado pelo compilador JIT. Logicamente faz parte do heap, mas é frequentemente chamada de "não-heap" para distinção.
No HotSpot, a área de métodos foi historicamente implementada como um "permanente" (PermGen), permitindo que o GC gerencie essa memória de forma similar ao heap. Esta região pode ou não ser submetida à coleta de lixo, dependendo da implementação. A falta de espaço na área de métodos também pode causar um erro OutOfMemoryError.
Pool de Constantes em Tempo de Execução
Uma sub-região da área de métodos, o pool de constantes em tempo de execução armazena constantes literais e referências simbólicas do arquivo de classe. Diferente do pool de constantes estático no arquivo .class, este pool pode ser modificado em tempo de execução, por exemplo, através do método intern() da classe String.
Criação de Objetos
O processo de criar um novo objeto na JVM envolve várias etapas, incluindo a alocação de memória no heap. O método de alocação depende da organização do heap:
- Alocação por Bumping de Ponteiro: Usada quando o heap é compacto, com memória livre contígua. Um ponteiro move-se para a frente para acomodar o novo objeto.
- Alocação por Lista Livre: Usada quando o heap é fragmentado. A JVM mantém uma lista de blocos de memória livres e aloca um bloco adequado.
A segurança em ambientes multi-thread é assegurada por mecanismos como CAS (Compare-And-Swap) com retry ou, mais eficientemente, pelo uso de TLABs, que permitem alocação privada por thread.
Disposição de Memória dos Objetos
No HotSpot, a memória de um objeto é dividida em três partes:
- Cabeça do Objeto: Contém metadados de execução como código hash, idade do GC, flags de bloqueio e um ponteiro de tipo que aponta para os metadados da classe. Para arrays, também inclui o comprimento do array.
- Dados da Instância: Os campos reais do objeto, incluindo aqueles herdados de superclasses. A ordem de armazenamento pode ser otimizada pela JVM.
- Preenchimento de Alinhamento: Dados de preenchimento para garantir que o objeto comece em um endereço múltiplo de 8 bytes, conforme exigido pela HotSpot.