Introdução
Java e Rust representam duas filosofias distintas para construir software confiável em larga escala. Java, com quase 30 anos de história, é a espinha dorsal de sistemas empresariais, bancos, telecoms e aplicações Android. Sua JVM (Java Virtual Machine) oferece portabilidade, um Garbage Collector maduro e um ecossistema massivo. Rust compila diretamente para código nativo, eliminando o GC em favor de ownership e borrowing, resultando em binários menores, startup instantâneo e consumo de memória previsível.
Este artigo é para desenvolvedores Java que estão avaliando Rust, arquitetos decidindo a stack de novos microsserviços, e qualquer pessoa curiosa sobre como essas linguagens se comparam na prática.
Tabela Comparativa
| Aspecto | Rust | Java |
|---|---|---|
| Execução | Código nativo (compilado, LLVM) | Bytecode JVM (JIT compilado) |
| Gerenciamento de memória | Ownership + Borrow Checker | Garbage Collector (G1, ZGC, Shenandoah) |
| Tempo de startup | < 1 ms | 100-500 ms (JVM warmup: segundos) |
| Consumo de memória | 5-20 MB (servidor típico) | 100-500 MB (JVM heap) |
| Concorrência | async/await + threads + channels | Virtual Threads (Loom), ExecutorService |
| Null safety | Option<T> (sem null) | Optional + anotações (null existe) |
| Generics | Monomorphization (zero-cost) | Type erasure (overhead em runtime) |
| Ecossistema | crates.io (~150k) | Maven Central (~600k artifacts) |
| Frameworks web | Axum, Actix Web | Spring Boot, Quarkus, Micronaut |
| Curva de aprendizado | Íngreme (ownership, lifetimes) | Moderada (verbosa, mas previsível) |
GC vs Ownership: O Trade-Off Central
Garbage Collection em Java
Java usa um Garbage Collector que periodicamente pausa a aplicação para identificar e liberar memória não utilizada. Os GCs modernos (ZGC, Shenandoah) reduziram as pausas para sub-milissegundos, mas o trade-off permanece:
import java.util.ArrayList;
import java.util.List;
public class ExemploGC {
record Pedido(String cliente, double valor) {}
public static void main(String[] args) {
List<Pedido> pedidos = new ArrayList<>();
// Milhões de alocações — o GC precisa gerenciar tudo
for (int i = 0; i < 1_000_000; i++) {
pedidos.add(new Pedido("Cliente " + i, Math.random() * 1000));
}
double total = pedidos.stream()
.filter(p -> p.valor() > 500)
.mapToDouble(Pedido::valor)
.sum();
System.out.printf("Total de pedidos acima de R$500: R$%.2f%n", total);
// Objetos temporários do stream são coletados pelo GC eventualmente
}
}
Ownership em Rust
Rust libera memória deterministicamente quando o owner sai de escopo — sem pausas, sem overhead:
struct Pedido {
cliente: String,
valor: f64,
}
fn main() {
let pedidos: Vec<Pedido> = (0..1_000_000)
.map(|i| Pedido {
cliente: format!("Cliente {i}"),
valor: rand::random::<f64>() * 1000.0,
})
.collect();
let total: f64 = pedidos
.iter()
.filter(|p| p.valor > 500.0)
.map(|p| p.valor)
.sum();
println!("Total de pedidos acima de R$500: R${total:.2}");
} // pedidos e todos os Pedido são liberados aqui, instantaneamente
A diferença em memória é dramática: o vetor Java consome ~2x mais memória devido ao overhead de objetos na JVM (headers, padding, referências boxed).
Exemplo Prático: API REST
Vamos comparar uma API REST simples para gerenciar tarefas.
Java com Spring Boot
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@SpringBootApplication
@RestController
@RequestMapping("/tarefas")
public class TarefaApp {
record Tarefa(Long id, String titulo, boolean concluida) {}
record CriarTarefa(String titulo) {}
private final Map<Long, Tarefa> tarefas = new ConcurrentHashMap<>();
private final AtomicLong contador = new AtomicLong();
@GetMapping
public Collection<Tarefa> listar() {
return tarefas.values();
}
@PostMapping
public Tarefa criar(@RequestBody CriarTarefa dto) {
long id = contador.incrementAndGet();
Tarefa tarefa = new Tarefa(id, dto.titulo(), false);
tarefas.put(id, tarefa);
return tarefa;
}
@PutMapping("/{id}/concluir")
public Tarefa concluir(@PathVariable Long id) {
Tarefa atual = tarefas.get(id);
if (atual == null) throw new RuntimeException("Tarefa não encontrada");
Tarefa concluida = new Tarefa(atual.id(), atual.titulo(), true);
tarefas.put(id, concluida);
return concluida;
}
public static void main(String[] args) {
SpringApplication.run(TarefaApp.class, args);
}
}
Rust com Axum
use axum::{
extract::{Path, State},
routing::{get, post, put},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{atomic::{AtomicU64, Ordering}, Arc, RwLock};
#[derive(Clone, Serialize)]
struct Tarefa {
id: u64,
titulo: String,
concluida: bool,
}
#[derive(Deserialize)]
struct CriarTarefa {
titulo: String,
}
type Db = Arc<RwLock<HashMap<u64, Tarefa>>>;
async fn listar(State(db): State<Db>) -> Json<Vec<Tarefa>> {
let tarefas = db.read().unwrap();
Json(tarefas.values().cloned().collect())
}
async fn criar(
State(db): State<Db>,
State(contador): State<Arc<AtomicU64>>,
Json(dto): Json<CriarTarefa>,
) -> Json<Tarefa> {
let id = contador.fetch_add(1, Ordering::Relaxed) + 1;
let tarefa = Tarefa { id, titulo: dto.titulo, concluida: false };
db.write().unwrap().insert(id, tarefa.clone());
Json(tarefa)
}
async fn concluir(
State(db): State<Db>,
Path(id): Path<u64>,
) -> Json<Tarefa> {
let mut tarefas = db.write().unwrap();
let tarefa = tarefas.get_mut(&id).expect("Tarefa não encontrada");
tarefa.concluida = true;
Json(tarefa.clone())
}
#[tokio::main]
async fn main() {
let db: Db = Arc::new(RwLock::new(HashMap::new()));
let contador = Arc::new(AtomicU64::new(0));
let app = Router::new()
.route("/tarefas", get(listar).post(criar))
.route("/tarefas/{id}/concluir", put(concluir))
.with_state((db, contador));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
Ambos os frameworks são produtivos, mas as diferenças aparecem em produção. Confira nosso tutorial de API REST com Axum para um guia completo.
Comparação de Performance
Benchmarks de Servidor Web
| Métrica | Rust (Axum) | Java (Spring Boot) | Java (Quarkus Nativo) |
|---|---|---|---|
| Requisições/s (JSON) | ~850.000 | ~120.000 | ~250.000 |
| Latência p99 | 0,8 ms | 8 ms | 3 ms |
| Startup time | < 5 ms | ~3.000 ms | ~50 ms (GraalVM) |
| Memória idle | ~5 MB | ~200 MB | ~40 MB |
| Memória sob carga | ~25 MB | ~500 MB | ~120 MB |
| Tamanho do binário | ~8 MB | ~40 MB (fat JAR) | ~60 MB (native) |
O Custo Real da JVM
A JVM é uma plataforma extraordinária, mas tem um custo:
- Startup lento: a JVM precisa carregar classes, compilar JIT, aquecer caches. Isso é problemático para serverless (AWS Lambda, Cloud Functions)
- Consumo de memória base: mesmo uma aplicação “Hello World” em Spring consome ~150 MB
- Pausas de GC: embora menores com ZGC, ainda existem e afetam latência p99
- Overhead de objetos: cada objeto Java tem 12-16 bytes de header
Rust não tem nenhum desses custos. O binário inicia em milissegundos, consome poucos MB e tem latência completamente previsível.
Onde Java se Recupera
Java brilha em:
- Throughput sustentado: após o warmup do JIT, a performance de Java pode se aproximar de código nativo
- Profiling e observabilidade: JFR, JMX, VisualVM — ferramentas de diagnóstico maduras
- Ecossistema empresarial: Spring, Hibernate, Kafka client, gRPC — tudo maduro e battle-tested
Null Safety: Option vs Optional
Um dos maiores problemas em Java é o NullPointerException. Rust simplesmente não tem null:
// Java: null é onipresente
String nome = mapa.get("chave"); // pode ser null
int tamanho = nome.length(); // NullPointerException em runtime!
// Com Optional (melhora, mas é opt-in)
Optional<String> nome = Optional.ofNullable(mapa.get("chave"));
int tamanho = nome.map(String::length).orElse(0);
// Rust: Option<T> é obrigatório — null não existe
let nome: Option<&str> = mapa.get("chave").map(|s| s.as_str());
// O compilador OBRIGA você a tratar o caso None
let tamanho = match nome {
Some(n) => n.len(),
None => 0,
};
// Ou de forma mais concisa:
let tamanho = nome.map_or(0, |n| n.len());
Quando Usar Java
Escolha Java quando:
- Projetos empresariais grandes: equipes de 50+ desenvolvedores, onde a uniformidade importa
- Ecossistema específico: Kafka, Hadoop, Elasticsearch, Android (legado)
- A equipe conhece Java profundamente: reaprender uma linguagem tem custo
- Spring Boot resolve o problema: CRUD, APIs, microserviços convencionais
- Hot reload importa: desenvolvimento iterativo rápido com Spring DevTools
Quando Usar Rust
Escolha Rust quando:
- Serverless/containers: onde startup time e memória são cobrados por uso
- Microsserviços de alta performance: baixa latência, alta concorrência
- Sistemas embarcados ou IoT: onde a JVM não cabe
- Processamento de dados em tempo real: sem pausas de GC
- CLI tools: binários estáticos sem dependências
- WebAssembly: Rust tem suporte muito superior para Wasm
Conclusão e Recomendação
Para novos microsserviços em 2026, se performance, consumo de recursos e startup rápido são importantes (especialmente em ambientes cloud/serverless), Rust com Axum é a melhor escolha. Você economiza em custos de infraestrutura e ganha em confiabilidade.
Para aplicações empresariais convencionais onde o ecossistema Spring/Jakarta EE já resolve o problema e a equipe é proficiente em Java, continue com Java. O investimento em migrar não se justifica para CRUDs típicos.
A tendência para 2026-2027 é clara: Rust está conquistando o nicho de “Java para microsserviços de alta performance”, enquanto Java mantém seu domínio em aplicações empresariais tradicionais. Considere usar ambas no mesmo sistema: Java para lógica de negócios complexa e Rust para componentes críticos de performance.
Veja Também
- Tutorial: API REST com Axum — Construa APIs completas em Rust
- Rust vs Go: Qual Escolher em 2026 — Outra alternativa para backend
- Rust vs Kotlin: Server-Side e Mobile — Compare com a alternativa moderna da JVM
- Receita: Criar Servidor HTTP — Exemplo prático de servidor web
- Instalação do Rust — Configure seu ambiente de desenvolvimento
- Glossário Rust — Termos como ownership, borrowing e traits