CodeGym /Cursos /JAVA 25 SELF /Compressão e perfilamento da serialização

Compressão e perfilamento da serialização

JAVA 25 SELF
Nível 45, Lição 4
Disponível

1. Introdução: por que otimizar a serialização?

Em aplicativos modernos, a serialização está em toda parte — de protocolos de rede a caches distribuídos e troca de dados entre serviços.

A velocidade e o tamanho da serialização são críticos. Se a serialização for lenta, o aplicativo começa a “ficar lento” ao salvar ou carregar dados, e a rede ou o disco ficam ociosos à toa. Se os objetos ficarem grandes demais, ocupam muito espaço em disco, demoram mais para trafegar na rede e geram carga extra sobre memória e largura de banda.

Tarefas típicas incluem salvar um grande grafo de objetos em arquivo ou cache, transmitir objetos pela rede com latência mínima e serializar/desserializar dados rapidamente em um sistema multithread.

A conclusão é simples: otimizar a serialização não é um “recurso premium”, mas uma prática necessária para aplicações de alto desempenho e escaláveis.

2. Otimização do tamanho dos dados serializados

Excluir dados desnecessários: a palavra‑chave transient

Por padrão, todos os campos do objeto são serializados, exceto os marcados como transient. Se um campo não precisa ser salvo (por exemplo, cache, dados temporários, referências a serviços), marque-o como transient:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // não será serializado
}

Vantagens:

  • Tamanho menor do objeto serializado.
  • Sem dados desnecessários/perigosos no arquivo ou na rede.

Serialização manual: interface Externalizable

Se você precisa de controle total sobre o que e como é serializado, implemente a interface Externalizable e descreva explicitamente a serialização (métodos writeExternal/readExternal):

public class Person implements Externalizable {
    private String name;
    private int age;
    private transient String secret;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
        // não serializamos secret
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        name = in.readUTF();
        age = in.readInt();
    }
}

Vantagens:

  • Apenas os campos necessários são serializados.
  • Você pode mudar o formato de serialização sem perder compatibilidade.

Compressão: reduzir os dados serializados

Objetos serializados costumam ocupar muito espaço, especialmente se contêm strings repetidas e coleções grandes. É possível reduzir o tamanho com compressão.

Exemplo com GZIPOutputStream:

try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data.gz")))) {
    out.writeObject(bigObject);
}

Exemplo com ZipOutputStream:

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("data.zip"))) {
    zip.putNextEntry(new ZipEntry("object"));
    ObjectOutputStream out = new ObjectOutputStream(zip);
    out.writeObject(bigObject);
    out.flush();
    zip.closeEntry();
}

Vantagens:

  • O tamanho do arquivo pode diminuir várias vezes (especialmente para grafos grandes de objetos).
  • Menos tráfego na transmissão pela rede.

Desvantagens:

  • Comprimir/descompactar exige tempo adicional (CPU).

3. Otimização da velocidade de serialização

Bufferização: por que usar BufferedOutputStream e BufferedInputStream

Problema:
Sem bufferização, cada chamada write() ou read() resulta em acesso do sistema ao disco ou à rede — isso é muito lento!

Solução:
Use streams com buffer:

try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
try (ObjectInputStream in = new ObjectInputStream(
         new BufferedInputStream(new FileInputStream("data.bin")))) {
    Object obj = in.readObject();
}

Vantagens:

  • Acelera significativamente a escrita/leitura de objetos grandes.
  • Reduz a quantidade de acessos ao disco/rede.

Como isso funciona?
O buffer acumula dados na memória e os grava em blocos, em vez de byte a byte.

Cópia rápida: FileChannel.transferTo

Se você precisa copiar rapidamente um arquivo serializado grande, use NIO e o método transferTo:

try (FileChannel src = new FileInputStream("data.bin").getChannel();
     FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
    src.transferTo(0, src.size(), dest);
}

Vantagens:

  • A cópia ocorre no nível do SO, contornando buffers extras no Java — muito rápido para arquivos grandes.

4. Perfilamento da serialização

Medição simples de tempo: System.nanoTime()

Para uma avaliação rápida do desempenho da serialização, você pode usar System.nanoTime():

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
long end = System.nanoTime();
System.out.println("Tempo de serialização: " + (end - start) / 1_000_000 + " ms");

Vantagens:

  • Simples e rápido.
  • Permite comparar diferentes variantes (com buffer, sem buffer, com compressão etc.).

Desvantagens:

  • Os resultados podem “oscilar” devido ao trabalho do GC e a processos em segundo plano.
  • Não é adequado para comparar diferenças microscópicas com precisão.

Perfilamento preciso: JMH (Java Microbenchmark Harness)

Para medições mais precisas, use JMH — uma biblioteca específica para microbenchmarks.

Exemplo de benchmark simples:

@Benchmark
public void serializeWithBuffer() throws Exception {
    try (ObjectOutputStream out = new ObjectOutputStream(
             new BufferedOutputStream(new FileOutputStream("data.bin")))) {
        out.writeObject(bigObject);
    }
}

Vantagens:

  • Considera o aquecimento da JVM, a influência do GC e os “ruídos” do SO.
  • Fornece resultados confiáveis e reproduzíveis.

Desvantagens:

  • Exige configuração e entendimento da metodologia do JMH.
  • Exagero para comparações “a olho”.

5. Prática: comparar tempo e tamanho da serialização

Vamos fazer um mini‑experimento: serializar um grande grafo de objetos (por exemplo, uma lista de 100_000 objetos com coleções aninhadas) de diferentes maneiras e comparar o tempo e o tamanho do arquivo.

Serialização sem bufferização e sem compressão

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Sem buffer: " + (end - start) / 1_000_000 + " ms, tamanho: " +
    new File("data1.bin").length() + " bytes");

Serialização com bufferização

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data2.bin")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Com buffer: " + (end - start) / 1_000_000 + " ms, tamanho: " +
    new File("data2.bin").length() + " bytes");

Serialização com compressão (GZIP)

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data3.gz")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Com compressão: " + (end - start) / 1_000_000 + " ms, tamanho: " +
    new File("data3.gz").length() + " bytes");

Análise dos resultados

Nos testes de serialização, fica nítido o quanto a bufferização e a compressão influenciam. Os arquivos compactados geralmente ficam de 2–10 vezes menores (o fator exato depende da estrutura dos dados). Com buffer, a serialização fica significativamente mais rápida, e a compressão deixa o processo um pouco mais lento, mas a economia de espaço costuma valer a pena.

Conclusão: para grandes volumes de dados, use obrigatoriamente bufferização; se o tamanho for crítico, ative a compressão.

6. Erros comuns ao otimizar a serialização

Erro nº 1: Não usar bufferização — a serialização de objetos grandes fica várias vezes mais lenta.

Erro nº 2: Serializar dados desnecessários ou sensíveis (por exemplo, senhas, tokens temporários) — sempre use transient para esses campos.

Erro nº 3: Esperar que a compressão sempre acelere a serialização — na verdade, a compressão reduz o tamanho, mas pode deixá-la um pouco mais lenta (especialmente em CPUs fracas).

Erro nº 4: Medir o tempo sem considerar o aquecimento da JVM e a influência do GC — para benchmarks precisos, use o JMH.

Erro nº 5: Comparar apenas o tempo ou apenas o tamanho — sempre observe ambos os parâmetros para escolher o equilíbrio ideal para o seu caso.

1
Tarefa
JAVA 25 SELF, nível 45, lição 4
Bloqueado
Arquivista de dados: compressão máxima de informações repetitivas
Arquivista de dados: compressão máxima de informações repetitivas
1
Tarefa
JAVA 25 SELF, nível 45, lição 4
Bloqueado
Desenvolvedor de jogos: avaliação do desempenho ao salvar o inventário
Desenvolvedor de jogos: avaliação do desempenho ao salvar o inventário
1
Pesquisa/teste
Otimização da serialização binária, nível 45, lição 4
Indisponível
Otimização da serialização binária
Otimização da serialização binária
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION