CodeGym /Cursos /JAVA 25 SELF /Variáveis locais, vazamentos de memória, referências frac...

Variáveis locais, vazamentos de memória, referências fracas

JAVA 25 SELF
Nível 64 , Lição 2
Disponível

1. Onde ficam armazenadas as variáveis locais

Vamos começar pelo mais simples: variáveis locais. São variáveis declaradas dentro de um método e que existem apenas durante sua execução. Sua vida é curta e dramática: assim que o método é concluído, todas as suas variáveis locais desaparecem sem deixar vestígios.

Em Java, as variáveis locais ficam na pilha. Cada thread tem sua própria pilha. Se um método chama outro método, é adicionado à pilha um novo “frame” (stack frame) com variáveis locais e endereço de retorno. Quando o método termina, seu frame é removido da pilha.

Exemplo: a vida de uma variável local

public class LocalVariableDemo {
    public static void main(String[] args) {
        int a = 42; // a variável local a vive apenas em main
        printSquare(a);
        // Aqui a variável b não existe mais!
    }

    public static void printSquare(int b) {
        int square = b * b; // variável local square
        System.out.println("Quadrado: " + square);
        // Após sair de printSquare, todas as variáveis locais desaparecem
    }
}

Ponto importante: se a variável local for uma referência a um objeto (por exemplo, String, Scanner, array), a própria referência vive na pilha, mas o objeto — no heap! Quando a referência desaparece e não há outras referências ao objeto, o coletor de lixo pode removê-lo.

Ilustração

Stack (para main):
| int a = 42      |
| args            |
-------------------
Heap:
| [objetos criados via new] |

2. Vazamentos de memória em Java: mito ou realidade?

Muitos iniciantes pensam: “Mas Java tem coletor de lixo! Então não pode haver vazamentos de memória!” Infelizmente, isso é um mito que se desfaz no primeiro projeto grande.

O coletor de lixo remove apenas objetos para os quais não existe nenhuma referência viva. Se restar alguma referência em algum lugar (mesmo no lugar mais inesperado), o objeto vai ficar na memória até o fim. Ou até um OutOfMemoryError.

Exemplo 1: Coleção estática como armadilha

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakDemo {
    // Ah, essa coleção estática!
    private static final List<String> BIG_LIST = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            BIG_LIST.add("Linha número " + i);
        }
        System.out.println("Um milhão de linhas adicionadas");
        // Mesmo que main termine, BIG_LIST permanecerá na memória enquanto a JVM estiver em execução
    }
}

O que está acontecendo?

  • A variável estática BIG_LIST vive enquanto a classe estiver carregada (geralmente até o fim da vida da JVM).
  • Todas as strings adicionadas à lista não podem ser removidas pelo coletor de lixo — sempre há uma referência a elas via BIG_LIST.
  • Se você esquecer de limpar tais coleções, terá um vazamento de memória.

Exemplo 2: Listeners não removidos (listeners)

import java.util.ArrayList;
import java.util.List;

class EventSource {
    private final List<Runnable> listeners = new ArrayList<>();

    public void addListener(Runnable listener) {
        listeners.add(listener);
    }

    // ... outros métodos ...
}

public class ListenerLeakDemo {
    public static void main(String[] args) {
        EventSource source = new EventSource();
        Runnable listener = () -> System.out.println("Evento!");
        source.addListener(listener);
        // Se você esquecer de chamar source.removeListener(listener), o listener ficará na memória para sempre!
    }
}

Problema: se o listener não for mais necessário e não for removido da lista, ele — e todos os objetos aos quais ele faz referência — permanecerão na memória.

Exemplo 3: Um cache que nunca é limpo

import java.util.HashMap;
import java.util.Map;

public class CacheLeakDemo {
    private static final Map<String, byte[]> CACHE = new HashMap<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            // A cada vez criamos um array de 1 KB
            CACHE.put("key" + i, new byte[1024]);
        }
        System.out.println("Adicionados um milhão de elementos ao cache");
        // O cache cresce, a memória acaba, OutOfMemoryError!
    }
}

Conclusão: mesmo com GC é fácil ter vazamento de memória se você não acompanhar o ciclo de vida dos objetos!

3. Referências fracas (WeakReference) e seus amigos

Às vezes precisamos de caches ou coleções onde os objetos possam ser removidos pelo coletor de lixo se ninguém mais fizer referência a eles. Para isso existem as referências fracas (WeakReference).

Referências comuns (strong)

String s = new String("hello"); // referência strong

O objeto s vai viver na memória enquanto existir pelo menos uma referência strong.

Referência fraca (WeakReference)

import java.lang.ref.WeakReference;

public class WeakRefDemo {
    public static void main(String[] args) {
        String strong = new String("Olá, mundo!");
        WeakReference<String> weak = new WeakReference<>(strong);

        System.out.println("Antes da limpeza: " + weak.get()); // há uma referência

        strong = null; // removemos a referência strong

        System.gc(); // pedimos ao GC para limpar a memória (não é garantido!)

        // Depois de algum tempo weak.get() pode se tornar null
        System.out.println("Após o GC: " + weak.get());
    }
}

Como isso funciona?

  • Enquanto houver pelo menos uma referência strong ao objeto, o GC não o removerá.
  • Se restarem apenas referências fracas, o objeto pode ser removido na próxima coleta de lixo.
  • O método weak.get() retorna o objeto se ele ainda estiver vivo, ou null se o objeto tiver sido removido.

Onde usar referências fracas?

O principal uso — caches onde não é crítico se o objeto for removido da memória. Por exemplo, se você faz cache de imagens, mas não quer que o cache ocupe toda a memória.

Exemplo: WeakHashMap

WeakHashMap é uma coleção em que as chaves são mantidas por referências fracas. Se mais ninguém fizer referência à chave, o par correspondente é removido do mapa.

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        Map<Object, String> map = new WeakHashMap<>();
        Object key = new Object();
        map.put(key, "Valor");

        System.out.println("Antes da limpeza: " + map);

        key = null; // removemos a referência strong para a chave

        System.gc(); // pedimos para o GC trabalhar

        // Depois de algum tempo o mapa ficará vazio!
        try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        System.out.println("Após o GC: " + map);
    }
}

Atenção: WeakHashMap funciona apenas para as chaves — os valores são mantidos por referências comuns (strong).

Soft, Weak, Phantom: toda a família de referências

Em Java existem quatro tipos de referências (ordenadas por “força”):

Tipo de referência Quando o GC remove o objeto? Onde se aplica?
Strong
Somente quando não há nenhuma referência strong Variáveis comuns, coleções
Soft
Quando há falta de memória Cache que se deseja manter por mais tempo
Weak
Na próxima passada do GC, se não houver referências strong Caches, WeakHashMap, listeners
Phantom
Após a finalização, para rastrear a remoção do objeto Tarefas especiais, limpeza fora do heap
  • SoftReference — o objeto é removido quando falta memória (bom para caches de imagens etc.).
  • WeakReference — é removido na primeira coleta de lixo se não houver outras referências.
  • PhantomReference — o tipo mais “fantasma”, usado para cenários complexos, raramente usado por iniciantes.

4. Prática: exemplo de vazamento de memória e sua correção

Exemplo de vazamento: lista estática

import java.util.ArrayList;
import java.util.List;

public class LeakExample {
    private static final List<byte[]> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100_000; i++) {
            list.add(new byte[1024 * 1024]); // 1 MB
            if (i % 10 == 0) System.out.println("Adicionados " + i + " MB");
        }
    }
}

O que vai acontecer?
O programa rapidamente consumirá toda a memória disponível e cairá com OutOfMemoryError, porque a lista estática mantém referências a todos os arrays criados.

Correção: usar referências fracas

Se não for crítico que todos os objetos estejam sempre acessíveis, você pode armazená-los por referências fracas:

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class LeakFixed {
    private static final List<WeakReference<byte[]>> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100_000; i++) {
            list.add(new WeakReference<>(new byte[1024 * 1024]));
            if (i % 10 == 0) System.out.println("Adicionados " + i + " MB");
            System.gc(); // Dica para o GC (não garante limpeza imediata!)
        }
    }
}

Agora os arrays podem ser removidos pelo GC se ninguém mais fizer referência a eles — a lista armazena apenas referências fracas.

5. Cenários típicos de vazamento de memória

  • Listeners de eventos: esqueceu de remover o listener — o objeto vive para sempre.
  • Coleções estáticas: caches que não são limpos, listas globais — tudo isso pode levar a vazamentos.
  • Classes internas e lambdas: se uma classe interna ou lambda captura uma referência ao objeto externo, ele não será removido enquanto o objeto externo viver.

Exemplo com classe interna

public class Outer {
    private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB

    public Runnable createTask() {
        // A classe interna anônima captura uma referência a Outer!
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("Tarefa em execução");
            }
        };
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        Runnable task = outer.createTask();
        // Mesmo se outer = null, task ainda mantém uma referência a bigArray!
    }
}

Solução: use classes internas estáticas ou extraia a lógica para classes separadas, para não manter referências desnecessárias.

6. Prática: usando referências fracas em um cache

Vamos adicionar a um aplicativo de estudo um cache simples usando WeakHashMap.

import java.util.Map;
import java.util.WeakHashMap;

public class ImageCache {
    private final Map<String, byte[]> cache = new WeakHashMap<>();

    public void put(String name, byte[] data) {
        cache.put(name, data);
    }

    public byte[] get(String name) {
        return cache.get(name);
    }

    public static void main(String[] args) {
        ImageCache cache = new ImageCache();
        cache.put("cat", new byte[1024 * 1024]); // 1 MB
        System.out.println("Gatinho adicionado ao cache");

        // Se não houver mais referências para a chave "cat", o objeto poderá ser removido pelo GC
    }
}

Em aplicações reais (por exemplo, em bibliotecas de imagens), referências fracas ajudam a evitar estouro de memória por meio da remoção automática de dados raramente usados.

7. Strong vs Weak: quando usar o quê?

  • Referências strong — padrão: use para tudo o que deve viver garantidamente.
  • Referências fracas — para caches, listeners, quando não é crítico se o objeto for removido.
  • Referências soft — para caches que você deseja manter por mais tempo, mas que podem ser removidos quando faltar memória.
  • Referências phantom — para cenários avançados (por exemplo, finalização fora do heap).

8. Erros comuns ao trabalhar com memória e referências

Erro nº 1: “O GC vai limpar tudo para mim!” O coletor de lixo remove apenas objetos para os quais não há nenhuma referência strong viva. Se você “esquecer” uma referência em algum lugar (por exemplo, em uma coleção static), o objeto viverá para sempre.

Erro nº 2: Listeners esquecidos. Adicionou um listener a um objeto, mas não o removeu ao destruir o objeto? O listener e tudo o que ele capturou permanecerão na memória.

Erro nº 3: Cache sem referências fracas. Usa um HashMap comum para um cache que deveria ser limpo automaticamente? Melhor usar WeakHashMap ou SoftReference.

Erro nº 4: Classes internas e lambdas capturam o objeto externo. Classes internas (e expressões lambda) mantêm implicitamente uma referência ao objeto externo. Se você armazena instâncias dessas classes por mais tempo do que o próprio objeto externo, terá um vazamento.

Erro nº 5: Esperar que o GC rode imediatamente. Chamar System.gc() não garante que a coleta de lixo ocorrerá na hora. É apenas um “pedido” para a JVM, não uma ordem.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION