Rust vs Java: Performance e Segurança | Rust Brasil

Rust vs Java: GC vs ownership, performance, concorrência, ecossistema e quando migrar. Comparação completa 2026.

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

AspectoRustJava
ExecuçãoCódigo nativo (compilado, LLVM)Bytecode JVM (JIT compilado)
Gerenciamento de memóriaOwnership + Borrow CheckerGarbage Collector (G1, ZGC, Shenandoah)
Tempo de startup< 1 ms100-500 ms (JVM warmup: segundos)
Consumo de memória5-20 MB (servidor típico)100-500 MB (JVM heap)
Concorrênciaasync/await + threads + channelsVirtual Threads (Loom), ExecutorService
Null safetyOption<T> (sem null)Optional + anotações (null existe)
GenericsMonomorphization (zero-cost)Type erasure (overhead em runtime)
Ecossistemacrates.io (~150k)Maven Central (~600k artifacts)
Frameworks webAxum, Actix WebSpring 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étricaRust (Axum)Java (Spring Boot)Java (Quarkus Nativo)
Requisições/s (JSON)~850.000~120.000~250.000
Latência p990,8 ms8 ms3 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