Divisão da Memória em Tempo de Execução
De acordo com a especificação do Java SE, a JVM divide a memória em diversas áreas funcionais, cada uma com propósitos específicos e ciclos de vida distintos.
1. Method Area (Área de Métodos)
Esta área é compartilhada por todas as threads e armazena estruturas de nível de classe, como o pool de constantes, dados de campos, códigos de métodos e construtores. No JDK 1.7 e anteriores, era conhecida como Permanent Generation (PermGen). A partir do JDK 1.8, a PermGen foi substituída pelo Metaspace, que utiliza memória nativa em vez de uma porção fixa do heap, reduzindo erros de OutOfMemoryError relacionados ao carregamento de classes.
2. Java Heap
É a maior área de memória da JVM, criada no início da aplicação e compartilhada entre todas as threads. Todos os objetos e arrays são alocados aqui. O heap é o alvo principal da coleta de lixo (Garbage Collection). É geralmente dividido em:
- Young Generation: Onde novos objetos são criados. Subdividida em Eden e dois Survivor Spaces (S0 e S1).
- Old Generation: Onde residem objetos que sobreviveram a múltiplos ciclos de coleta na Young Generation.
3. Java Virtual Machine Stacks
Cada thread possui sua própria stack (pilha), criada simultaneamente à thread. Ela armazena frames, que contêm variáveis locais, resultados intermediários e dados para invocação e retorno de métodos. Cada chamada de método resulta na criação de um novo frame que é empilhdao e removido após a execução.
4. Program Counter (PC) Register
Uma pequena área de memória que armazena o endereço da instrução da JVM que está sendo executada no momento pela thread atual. Cada thread possui seu próprio PC Register.
5. Native Method Stacks
Semelhante à stack da JVM, mas utilizada para a execução de métodos nativos (escritos em linguagens como C ou C++).
Análise de Estouro de Memória (OutOfMemoryError)
Heap Space Overflow
Ocorre quando a aplicação continua criando objetos sem liberar as referências, impedindo o Garbage Collector de recuperar o espaço. Eventualmente, o heap atinge seu limite máximo (-Xmx).
// Configuração sugerida: -Xms20m -Xmx20m
import java.util.LinkedList;
import java.util.List;
public class SimuladorHeap {
static class UnidadeDados {
byte[] payload = new byte[1024 * 1024]; // 1MB
}
public static void main(String[] args) {
List<UnidadeDados> lista = new LinkedList<>();
while (true) {
lista.add(new UnidadeDados());
}
}
}
Stack Overflow
Geralemnte causado por recursão excessiva ou profundidade de chamada de método que excede o limite da pilha definido por -Xss.
// Configuração sugerida: -Xss256k
public class AnaliseStack {
private int profundidade = 0;
public void executarChamada() {
profundidade++;
executarChamada();
}
public static void main(String[] args) {
AnaliseStack analise = new AnaliseStack();
try {
analise.executarChamada();
} catch (StackOverflowError e) {
System.err.println("Limite atingido em: " + analise.profundidade);
throw e;
}
}
}
Algoritmos de Garbage Collection (GC)
Critérios de Sobrevivência
A JVM determina se um objeto é "lixo" através da Análise de Alcançabilidade. Partindo de GC Roots (como variáveis locais na stack ou variáveis estáticas na Method Area), o sistema mapeia todos os objetos alcançáveis. Aqueles sem um caminho até uma GC Root são marcados para coleta.
Estratégias Comuns
- Mark-Sweep (Marcar e Varrer): Identifica objetos inúteis e os remove. Gera fragmentação de memória.
- Copying (Cópia): Divide a memória ao meio e copia os sobreviventes para uma nova área, limpando a antiga. Eficiente para a Young Generation.
- Mark-Compact (Marcar e Compactar): Move os objetos sobreviventes para uma extremidade da memória, eliminando a fragmentação. Comum na Old Generation.
Coletores de Lixo
Serial e Paralel Collectors
O Serial Collector utiliza uma única thread para o GC, causando pausas "Stop The World" completas. O Parallel Scavenge é focado em alto throughput, utilizando múltiplas threads para acelerar a coleta.
CMS (Concurrent Mark Sweep)
Focado em baixa latência, tenta realizar a maior parte do trabalho de marcação e varredura simultaneamente com as threads da aplicação.
G1 (Garbage First)
Divide o heap em múltiplas regiões independentes. Ele prioriza a coleta das regiões com mais lixo, permitindo prever e limitar o tempo de pausa (pausa curta e controlada). É o coletor padrão a partir do Java 9.
Ferramentas de Monitoramento e Ajuste
O ecossistema Java fornece ferramentas robustas para diagnóstico de performance:
jps: Lista os processos JVM ativos.jstat: Monitora estatísticas de GC e uso de memória em tempo real.jmap: Gera dumps de memória (heap dump) para análise posterior.jstack: Captura o estado de todas as threads de um processo (thread dump).- VisualVM / JConsole: Interfaces gráficas para monitoramento de threads, CPU e heap.
Processo de Carregamento de Classes (ClassLoader)
O carregamento de uma classe passa por três etapas principais: Carregamento, Ligação (Verificação, Preparação e Resolução) e Inicialização.
Modelo de Delegação
A JVM utiliza um modelo de hierarquia para carregadores de classe:
- Bootstrap ClassLoader: Carrega as classes base do Java (RT.jar).
- Extension ClassLoader: Carrega classes de extensões instaladas.
- Application ClassLoader: Carrega as classes definidas no classpath do usuário.
Uma classe é delegada para o pai antes de ser tentada pelo carregador atual, garantindo a integridade do sistema (evitando que uma classe core como java.lang.Object seja sobrescrita).
Ordem de Execução de Inicialização
O código abaixo demonstra a ordem de execução entre blocos estáticos, blocos de instância e construtores durante o ciclo de vida de uma classe.
class Base {
static { System.out.println("1. Estático Base"); }
{ System.out.println("3. Instância Base"); }
public Base() { System.out.println("4. Construtor Base"); }
}
public class SubClasse extends Base {
static { System.out.println("2. Estático Sub"); }
{ System.out.println("5. Instância Sub"); }
public SubClasse() { System.out.println("6. Construtor Sub"); }
public static void main(String[] args) {
new SubClasse();
}
}