Vazamento de memória (memory leak) — situação em que um aplicativo não libera a memória ocupada por objetos que não são mais necessários. No desenvolvimento móvel, isso é especialmente crítico: o heap limitado e a ausência de swap levam a OutOfMemoryError e falhas do aplicativo. De acordo com a Purdue University (2022), 35% dos aplicativos Android no Google Play contêm pelo menos um vazamento de memória. Vamos analisar cenários típicos, ferramentas de diagnóstico e métodos de correção.
Principais pontos
Vazamento de memória é uma situação em que a memória alocada não é retornada ao sistema depois que o objeto não é mais necessário para o programa. O coletor de lixo considera esse objeto como vivo porque há uma cadeia de referências ativa de um GC Root apontando para ele.
Em Java/Kotlin, o coletor de lixo funciona automaticamente, mas não consegue determinar que um objeto é logicamente desnecessário se houver uma referência técnica para ele. O desenvolvedor deve quebrar explicitamente as conexões desnecessárias. Em Swift/Objective-C, o ARC conta referências automaticamente, mas os retain cycles bloqueiam a diminuição do contador.
O principal perigo dos vazamentos é o efeito cumulativo. Cada vazamento consome uma pequena quantidade de memória, mas com transições repetidas entre telas (rotações de tela, abertura/fechamento de Activities), os vazamentos se acumulam até esgotar o limite do heap.
Vazamento — o objeto está inacessível ao código, mas não é removido pelo GC. Inchaço — o objeto é logicamente necessário, mas é armazenado em quantidade excessiva. Exemplo de inchaço: um cache de imagens de 100 MB com um conjunto de trabalho de 30 MB. Ambos os problemas levam a OOM, mas as causas e os métodos de tratamento são diferentes.
ART (Android Runtime) usa coleta de lixo geracional com compactação concorrente. A memória é dividida em geração jovem (Young), geração velha (Old) e objetos grandes (Large). Objetos que sobrevivem a vários ciclos de GC são movidos para a geração Old, onde a coleta ocorre com menos frequência — isso acelera os ciclos normais.
O GC começa quando o heap atinge um certo limite de ocupação (normalmente 75-85%). Durante o GC, todas as threads do aplicativo são pausadas (STW — Stop The World). Quanto mais objetos vivos, mais longa a pausa. Os vazamentos aumentam o número de objetos vivos, prolongando as pausas do GC.
O coletor determina objetos vivos percorrendo o gráfico a partir das GC Roots: campos estáticos, variáveis de pilha de threads ativas, referências JNI. Qualquer objeto alcançável por referências a partir dessas raízes é considerado vivo — mesmo que o desenvolvedor saiba que não é mais necessário.
// Exemplo: coleção estática como GC Root — vazamento permanente
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference não impede o GC — comportamento correto
}
}
WeakReference resolve o problema: o GC ignora referências fracas ao determinar objetos vivos. Se apenas referências fracas restarem para um objeto, ele será coletado no ciclo de GC mais próximo.
Activity Context — o cenário de vazamento mais massivo no Android. Se um singleton, campo estático ou serviço de longa duração armazena uma referência a um Activity Context, toda a Activity com todas as suas Views não pode ser coletada pelo GC. Solução: use Application Context para objetos de longa duração.
Handler e mensagens enviadas — Handler.postDelayed(runnable, delay) coloca uma mensagem na fila do Main Looper. Se a Activity for destruída antes do término do atraso, a mensagem ainda está na fila e mantém uma referência através de Runnable → classe anônima → classe externa (Activity).
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // obrigatório: limpar fila
super.onPause()
}
}
Classes internas — uma classe interna não estática tem uma referência implícita à instância da classe externa. Se a classe externa for uma Activity e a classe interna for passada para algum lugar externo (por exemplo, para RecyclerView.Adapter), a Activity não pode ser coletada.
Android Studio Memory Profiler — ferramenta integrada para monitoramento de heap em tempo real. Mostra um gráfico de memória usada, número de alocações e objetos por tipo. Permite gravar um heap dump e exportá-lo no formato HPROF para análise no MAT.
Eclipse MAT (Memory Analyzer Tool) — analisador de desktop de heap dump. Constrói automaticamente relatórios de Leak Suspects, destacando objetos com o maior retained size e sugerindo a possível cadeia de GC Root para cada objeto suspeito.
Xcode Memory Graph Debugger — para iOS. Pausa o aplicativo e visualiza o gráfico de objetos. Retain cycles são destacados em vermelho; você pode clicar em qualquer objeto para ver seu retain count e referências.
| Ferramenta | Capacidades | Complexidade |
|---|---|---|
| Memory Profiler | Gráfico em tempo real, heap dump, Object Allocation Tracking | Baixa |
| Eclipse MAT | Dominator tree, Leak Suspects, consultas OQL | Média |
| LeakCanary | Detecção automática, trace de vazamento em notificação | Mínima |
| Xcode Memory Graph | Gráfico visual de retain cycles, lista de objetos vivos | Baixa |
De acordo com o Uber Engineering Blog, a integração de perfilamento automático de memória (LeakCanary + análise de heap dump) no pipeline CI/CD reduz incidentes relacionados à memória em produção em 60% em 3 meses.
Substituir Context — se um objeto sobreviver à Activity, use applicationContext. Todos os objetos de longa duração (singletons, repositórios, helpers de banco de dados) devem receber Application Context, não Activity Context. Exceção: componentes de UI que precisam de acesso ao tema ou recursos específicos da Activity.
Componentes conscientes do ciclo de vida — usar LifecycleObserver, DefaultLifecycleObserver ou extensões reativas cancela automaticamente as assinaturas em onDestroy. O Android Jetpack fornece lifecycleScope e viewModelScope, que são limpos pelo evento de ciclo de vida correspondente.
Classe interna estática — se a classe interna não precisar de acesso aos campos da classe externa, torne-a static. Uma classe interna estática não tem referência implícita à classe externa. Se o acesso for necessário, use WeakReference para a referência explícita.
class MyActivity : AppCompatActivity() {
// ❌ Classe interna não estática — referência implícita a MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Classe interna estática — sem referência implícita
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
Em iOS, use listas de captura: [weak self] em closures que podem sobreviver ao seu criador. Para delegados, use referências fracas (weak var delegate). Para closures que são garantidas de serem chamadas apenas durante a vida de self, [unowned self] pode ser usado, mas com cuidado — acessar um objeto desalocado causará uma falha.
Perguntas frequentes
No Android, execute várias transições entre telas (Activity A → B → A → B) e verifique adb shell dumpsys meminfo package_name. Se o Total PSS crescer de forma estável e não retornar ao valor original — há um vazamento. No iOS, similarmente: use Debug Memory Graph no Xcode para inspeção visual.
Sim, se o CoroutineScope não for cancelado quando o componente for destruído. Uma corrotina lançada em GlobalScope continua executando mesmo após finish() da Activity. Solução: use viewModelScope (cancelado em onCleared) ou lifecycleScope (cancelado em onDestroy). Para scopes personalizados, crie scopes conscientes do ciclo de vida através de LifecycleOwner.
Bitmap armazena dados de pixel no heap nativo, não no heap Java. Isso significa que o GC Java não vê o tamanho real do Bitmap. Se o Bitmap não for reciclado com recycle() ou a referência não for anulada, a memória nativa não será liberada. Use BitmapFactory com inSampleSize para carregar cópias reduzidas e Glide/Coil para gerenciamento automático de cache.
Um campo estático é uma GC Root. Ele vive enquanto a classe estiver carregada (no Android — enquanto o Process estiver vivo). Se um campo estático referenciar uma Activity, Bitmap, View ou qualquer outro objeto pesado, esse objeto nunca será coletado pelo GC. Um campo estático é uma referência eterna. Solução: armazene apenas WeakReference ou anule o campo estático em onDestroy.
ARC libera objetos automaticamente quando a contagem de referências fortes chega a zero. Um retain cycle é a única forma de vazamento com ARC. Sempre use weak para referências pai→filho onde o filho pode sobreviver ao pai (delegados, data sources). Para closures, use a lista de captura [weak self] e verifique se self não é nil dentro da closure.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também