┌─────────────────────────────────────────────────────────────────┐
│ JVM (Java Virtual Machine) │
│ │
│ ┌──────────────┐ ┌────────────────────────────────────┐ │
│ │ ClassLoader │ │ Runtime Data Areas │ │
│ │ │ │ ┌──────────┐ ┌─────────────────┐ │ │
│ │ Bootstrap │ │ │ Heap │ │ Stack (Thread) │ │ │
│ │ Extension │───>│ │ Young │ │ Frame │ │ │
│ │ Application │ │ │ Old Gen │ │ Local vars │ │ │
│ └──────────────┘ │ └──────────┘ └─────────────────┘ │ │
│ │ ┌──────────┐ ┌─────────────────┐ │ │
│ ┌──────────────┐ │ │ Metaspace│ │ PC Register │ │ │
│ │ Execution │ │ │ (classes)│ │ Native Stack │ │ │
│ │ Engine │ │ └──────────┘ └─────────────────┘ │ │
│ │ Interpreter │ └────────────────────────────────────┘ │
│ │ JIT Compiler│ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Carrega classes .class (bytecode) para a memória sob demanda. Hierarquia:
- Bootstrap: carrega classes do próprio Java (
java.lang,java.util) - Extension: carrega extensões do JDK
- Application: carrega as classes da sua aplicação
Young Generation Old Generation (Tenured)
┌──────────────────────────┐ ┌─────────────────────────────────┐
│ Eden Space │ │ │
│ (novos objetos) │ │ Objetos que sobreviveram │
│ ────────────────────── │ │ múltiplos GCs │
│ Survivor 0 Survivor 1 │ │ │
└──────────────────────────┘ └─────────────────────────────────┘
Minor GC (frequente) Major GC / Full GC (raro, lento)
Ciclo de vida de um objeto:
- Nasce no Eden Space
- Sobrevive ao Minor GC → vai para Survivor Space
- Sobrevive a N GCs (threshold) → promovido para Old Generation
- Não tem mais referências → coletado pelo GC
Cada thread tem sua própria stack. Cada chamada de método cria um frame com:
- Variáveis locais
- Operandos parciais da expressão atual
- Referência para o pool de constantes da classe
StackOverflowError ocorre quando a stack enche (geralmente por recursão infinita).
Armazena metadados das classes (estrutura, métodos, campos). Antes era PermGen com tamanho fixo — era comum OutOfMemoryError: PermGen space. Desde Java 8, Metaspace usa memória nativa e cresce automaticamente.
Usa uma única thread para GC. Para tudo durante a coleta ("stop-the-world").
java -XX:+UseSerialGC -jar app.jar
# Uso: aplicações pequenas, ambientes com uma CPUUsa múltiplas threads para GC, mas ainda para a aplicação durante a coleta.
java -XX:+UseParallelGC -XX:ParallelGCThreads=4 -jar app.jar
# Uso: aplicações batch que priorizam throughputDivide o heap em regiões. Tenta atingir um objetivo de pausa configurável. Bom equilíbrio entre throughput e latência.
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar
# Uso: aplicações de propósito geral (padrão)Pausa máxima de 1ms, independente do tamanho do heap. Ideal para aplicações com latência crítica ou heaps gigantes (TB de RAM).
java -XX:+UseZGC -jar app.jar
# Uso: aplicações com SLA de latência rigorosoSimilar ao ZGC, desenvolvido pela Red Hat. Pausas muito curtas (sub-millisecond).
java -XX:+UseShenandoahGC -jar app.jar
# Uso: similar ao ZGC| GC | Throughput | Latência | Memória | Uso recomendado |
|---|---|---|---|---|
| Serial | Baixo | Alta | Baixo | Apps pequenas |
| Parallel | Alto | Alta | Médio | Batch processing |
| G1 | Bom | Média | Médio | Padrão, apps web |
| ZGC | Bom | Muito baixa | Alto | Latência crítica |
| Shenandoah | Bom | Muito baixa | Alto | Latência crítica |
# ─── Tamanho do Heap ───────────────────────────────────────────
-Xms512m # Heap mínimo (inicial)
-Xmx2g # Heap máximo
# Dica: Xms = Xmx evita redimensionamento do heap
# ─── Em containers (melhor que -Xmx fixo) ─────────────────────
-XX:MaxRAMPercentage=75.0 # Usa 75% da RAM do container
-XX:InitialRAMPercentage=50.0
# ─── Garbage Collector ─────────────────────────────────────────
-XX:+UseG1GC # G1 (padrão Java 9+)
-XX:+UseZGC # ZGC (baixa latência)
-XX:MaxGCPauseMillis=200 # Objetivo de pausa máxima (G1)
# ─── GC Logging (para análise) ─────────────────────────────────
-Xlog:gc*:file=/logs/gc.log:time,uptime:filecount=5,filesize=20m
# ─── JIT ───────────────────────────────────────────────────────
-XX:+TieredCompilation # Habilitado por padrão
-XX:CompileThreshold=10000 # Métodos compilados pelo JIT após 10000 invocações
# ─── Diagnóstico ───────────────────────────────────────────────
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
-XX:+PrintGCDetails # Detalhes do GC no stdout
# ─── Performance ───────────────────────────────────────────────
-Djava.security.egd=file:/dev/./urandom # Evita bloqueio em geração de números aleatórios
-server # Otimizações para servidor (padrão em 64-bit)ENV JAVA_OPTS="\
-XX:+UseZGC \
-XX:MaxRAMPercentage=75.0 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof \
-Djava.security.egd=file:/dev/./urandom \
-Dspring.profiles.active=prod"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]VisualVM é uma ferramenta gratuita para monitorar e analisar a JVM em tempo real.
# Download
https://visualvm.github.io/download.html
# Ou via SDK Man
sdk install visualvm- Inicie sua aplicação com JMX habilitado:
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-jar app.jar- Abra o VisualVM
- No painel esquerdo, sua aplicação aparece automaticamente se local
- Para remota:
File → Add JMX Connection→host:9010
Monitor: CPU, Heap, Threads em tempo real
Sampler: veja quais métodos consomem mais CPU sem overhead
Profiler: análise mais detalhada, mas com mais overhead
Heap Dump: tire um snapshot do heap e analise:
- Quais classes têm mais instâncias
- O que está impedindo o GC de coletar
Thread Dump: captura o estado de todas as threads — essencial para diagnosticar deadlocks
JMH é a ferramenta oficial para escrever benchmarks corretos em Java. Mede nanossegundos com precisão, evitando armadilhas como JIT warm-up e dead code elimination.
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>@BenchmarkMode(Mode.AverageTime) // Tempo médio por operação
@OutputTimeUnit(TimeUnit.NANOSECONDS) // Unidade de saída
@State(Scope.Benchmark) // Estado compartilhado entre iterações
@Warmup(iterations = 5, time = 1) // 5 iterações de warm-up (descartadas)
@Measurement(iterations = 10, time = 1) // 10 iterações de medição
@Fork(2) // Roda em 2 JVMs separadas (evita viés)
public class StringConcatenationBenchmark {
@Param({"10", "100", "1000"})
private int tamanho; // Parametrização automática
private List<String> palavras;
@Setup // Executado antes dos benchmarks
public void setup() {
palavras = new ArrayList<>();
for (int i = 0; i < tamanho; i++) {
palavras.add("palavra" + i);
}
}
@Benchmark
public String concatenacaoNaive() {
String resultado = "";
for (String p : palavras) {
resultado += p; // O(n²) — realoca a cada iteração
}
return resultado;
}
@Benchmark
public String comStringBuilder() {
StringBuilder sb = new StringBuilder();
for (String p : palavras) {
sb.append(p); // O(n) — amortizado
}
return sb.toString();
}
@Benchmark
public String comStringJoin() {
return String.join("", palavras); // API mais legível, mesmo desempenho do StringBuilder
}
}# Executar os benchmarks
mvn package
java -jar target/benchmarks.jar
# Executar benchmark específico
java -jar target/benchmarks.jar StringConcatenationBenchmark
# Saída típica:
# Benchmark (tamanho) Mode Cnt Score Units
# StringConcatenationBenchmark.concatenacaoNaive 100 avgt 20 8423.2 ns/op
# StringConcatenationBenchmark.comStringBuilder 100 avgt 20 82.1 ns/op ← 100x mais rápido!
# StringConcatenationBenchmark.comStringJoin 100 avgt 20 80.3 ns/op// PROBLEMA: cache estático cresce indefinidamente
public class CacheProblematico {
private static final Map<String, byte[]> cache = new HashMap<>();
public byte[] getImagem(String url) {
return cache.computeIfAbsent(url, this::baixarImagem); // Cache sem limite!
}
}
// SOLUÇÃO: usar WeakReference ou Caffeine com tamanho máximo
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
public class CacheCorreto {
private final Cache<String, byte[]> cache = Caffeine.newBuilder()
.maximumSize(1000) // Tamanho máximo
.expireAfterAccess(10, TimeUnit.MINUTES) // TTL
.build();
public byte[] getImagem(String url) {
return cache.get(url, this::baixarImagem);
}
}// PROBLEMA: 1 query para buscar pedidos + N queries para buscar itens
@GetMapping("/pedidos")
public List<PedidoResponse> listar() {
List<Pedido> pedidos = pedidoRepository.findAll(); // 1 query
return pedidos.stream()
.map(p -> {
List<Item> itens = itemRepository.findByPedidoId(p.getId()); // N queries!
return new PedidoResponse(p, itens);
})
.toList();
}
// SOLUÇÃO 1: JOIN FETCH no JPQL
@Query("SELECT p FROM Pedido p JOIN FETCH p.itens WHERE p.status = :status")
List<Pedido> findByStatusComItens(@Param("status") String status);
// SOLUÇÃO 2: EntityGraph (mais flexível)
@EntityGraph(attributePaths = {"itens", "cliente"})
List<Pedido> findAll();
// SOLUÇÃO 3: Ativar log de SQL para detectar em desenvolvimento
# application.yml
spring.jpa.show-sql: true
logging.level.org.hibernate.SQL: DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder: TRACE// PROBLEMA: auto-boxing cria objetos Integer desnecessariamente
List<Integer> numeros = new ArrayList<>();
long soma = 0;
for (int i = 0; i < 1_000_000; i++) {
numeros.add(i); // boxing: int → Integer (cria objeto)
soma += numeros.get(i); // unboxing: Integer → int
}
// SOLUÇÃO: usar tipos primitivos com coleções especializadas
// Opção 1: IntStream (para operações encadeadas)
long soma2 = IntStream.range(0, 1_000_000).asLongStream().sum();
// Opção 2: int[] (sem overhead de boxing)
int[] numeros2 = new int[1_000_000];
long soma3 = 0;
for (int i = 0; i < numeros2.length; i++) {
numeros2[i] = i;
soma3 += numeros2[i]; // sem boxing
}
// Opção 3: biblioteca Eclipse Collections (primitives collections)
// LongList, IntList etc. sem boxing// PROBLEMA: O(n²) — cria nova String a cada iteração
String resultado = "";
for (String linha : milharesDeLinha) {
resultado += linha + "\n"; // Ruim!
}
// SOLUÇÃO: StringBuilder — O(n)
StringBuilder sb = new StringBuilder(milharesDeLinha.size() * 100); // capacidade inicial
for (String linha : milharesDeLinha) {
sb.append(linha).append('\n'); // Use char em vez de String de 1 char
}
String resultado2 = sb.toString();
// Nota: dentro de um único statement, o Java otimiza automaticamente
// "A" + "B" + "C" → o compilador usa StringBuilder internamente
// O problema só existe em LOOPSGraalVM Native Image compila o bytecode Java para um executável nativo específico do SO, sem precisar de JVM em runtime.
Compilação JIT (tradicional): Compilação AOT (Native Image):
código fonte código fonte
↓ javac ↓ javac
bytecode (.class) bytecode (.class)
↓ JVM ↓ native-image
interpretado → JIT binário nativo (ELF/Mach-O/PE)
(aquece com o tempo) (executa direto no OS)
<!-- pom.xml: adicionar perfil para build nativo -->
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals><goal>compile-no-fork</goal></goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>minha-app</imageName>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-O2</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles># Build nativo (requer GraalVM instalado)
./mvnw package -Pnative -DskipTests
# Executar
./target/minha-app
# Comparação de startup:
# JVM: Started MinhaAppApplication in 3.241 seconds
# Native: Started MinhaAppApplication in 0.048 seconds ← 67x mais rápido!// ANTES: cria String intermediária a cada chamada
public boolean isAdmin(String role) {
return role.toUpperCase().equals("ADMIN"); // cria nova String
}
// DEPOIS: sem alocação
public boolean isAdmin(String role) {
return "ADMIN".equalsIgnoreCase(role); // sem nova String
}// ANTES: Collections.unmodifiableList ainda permite modificar a lista original
List<String> original = new ArrayList<>(List.of("a", "b", "c"));
List<String> imutavel = Collections.unmodifiableList(original);
original.add("d"); // imutavel também muda!
// DEPOIS: List.copyOf faz cópia real e é verdadeiramente imutável
List<String> imutavel2 = List.copyOf(original); // cópia independente// ANTES: parallelStream com operação IO-bound (causa starvation do ForkJoinPool)
List<Produto> produtos = ids.parallelStream()
.map(id -> repository.findById(id)) // IO: bloqueia threads do ForkJoinPool!
.toList();
// DEPOIS: para IO-bound, use CompletableFuture com executor próprio
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
List<Produto> produtos2 = ids.stream()
.map(id -> CompletableFuture.supplyAsync(() -> repository.findById(id), executor))
.map(CompletableFuture::join)
.toList();
// parallelStream é bom para: operações CPU-bound sem IO
List<Integer> resultados = numeros.parallelStream()
.map(n -> calcularComplexo(n)) // CPU-bound: OK com parallelStream
.toList();// ANTES: calcula tudo no construtor, mesmo que nunca use
public class RelatorioService {
private final List<Configuracao> configuracoes;
public RelatorioService() {
this.configuracoes = carregarTodasConfiguracoes(); // Sempre executado
}
}
// DEPOIS: inicializa sob demanda
public class RelatorioService {
private List<Configuracao> configuracoes;
public List<Configuracao> getConfiguracoes() {
if (configuracoes == null) {
configuracoes = carregarTodasConfiguracoes(); // Só quando precisar
}
return configuracoes;
}
}Agora que você entende o que o ClassLoader faz, o ciclo de JIT compilation e por que um fat JAR Spring Boot demora para inicializar, o GraalVM Native Image faz sentido.
O compilador AOT (Ahead-of-Time) do GraalVM faz uma análise estática completa da aplicação — rastreia todas as classes alcançáveis, todos os métodos chamados, todas as dependências — e produz um executável nativo que não precisa da JVM em runtime. Não há ClassLoader carregando classes sob demanda. Não há JIT compilando bytecode progressivamente. O código já sai como binário nativo.
O resultado: startup em ~50ms em vez de 3-8 segundos, uso de memória 3-5x menor, e um executável único. Para uma função Lambda que precisa responder em menos de 100ms ou um microsserviço que escala para zero quando não há tráfego, isso muda completamente o cálculo de custo.
A análise estática do GraalVM precisa saber em tempo de build o que será carregado em tempo de execução. Reflexão é o oposto disso: Class.forName("com.empresa.SomeClass") decide em runtime qual classe carregar. Spring Boot usa reflexão extensivamente para injeção de dependências, serialização/desserialização, e proxies AOP.
A solução é declarar o que precisa de reflexão via hints:
// Classe marcada para ser incluída na análise de reflexão
@RegisterReflectionForBinding(ProdutoDTO.class)
public class ProdutoController { }
// Ou via anotação em nível de pacote (mais comum com Spring Boot AOT)
// O Spring Boot 3+ gera a maioria dos hints automaticamente durante o buildQuando você vê um ClassNotFoundException ou NoSuchMethodException em runtime depois de um build nativo — e o código funcionava normalmente na JVM — reflexão não declarada é quase certamente o culpado.
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.3</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
<configuration>
<imageName>sistema-financeiro</imageName>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
<buildArg>-O3</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>minha-empresa/sistema-financeiro:native</name>
<builder>paketobuildpacks/builder-jammy-tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles># Via SDKMAN (recomendado)
sdk install java 21.0.4-graal
sdk use java 21.0.4-graal
# Build nativo (espere 5-15 minutos — isso é normal)
mvn -Pnative package
# O executável estará em target/
./target/sistema-financeiro
# Startup em ~50ms, sem JVM
# Gerar imagem Docker nativa sem instalar GraalVM localmente
mvn spring-boot:build-image -Pnative
docker run --rm minha-empresa/sistema-financeiro:nativeO build de 15 minutos é real. Sem hot reload durante desenvolvimento é real. Ferramentas de profiling (JMC, async-profiler) que dependem da JVM não funcionam com o executável nativo. Não faz sentido para aplicações monolíticas com startup irrelevante onde a JVM já aquecida tem throughput superior.
Faz sentido para: funções serverless (Lambda, Cloud Functions), CLIs distribuídas como executável único, microsserviços com autoescalamento agressivo onde o custo de memória idle importa, e containers em ambientes com restrição severa de memória.