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? |
|---|---|---|
|
Somente quando não há nenhuma referência strong | Variáveis comuns, coleções |
|
Quando há falta de memória | Cache que se deseja manter por mais tempo |
|
Na próxima passada do GC, se não houver referências strong | Caches, WeakHashMap, listeners |
|
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.
GO TO FULL VERSION