Tudo o que você precisa saber sobre vazamentos de memória no Android.

Ali Asadi
30 de Junho de 2019 – 16 min. lido

Um dos principais benefícios do Java, ou, para ser mais preciso, da JVM (Máquina Virtual Java), é o coletor de lixo (GC). Podemos criar novos objectos sem nos preocuparmos em libertá-los da memória. O lixeiro se encarregará de alocar e liberar a memória para nós.

Não exatamente! Podemos evitar que o coletor de lixo liberte a memória para nós se não entendermos totalmente como o GC funciona.

Escrever um código sem um bom entendimento de como o GC funciona poderia fazer vazamentos de memória no aplicativo. Esses vazamentos podem afetar nossa aplicação desperdiçando memória não liberada e, eventualmente, fazer com que haja exceções e lapsos de memória.

O que é vazamento de memória?

Failure to release unused objects from the memory

Failure to release unused objects from the memory significa que há objetos não utilizados na aplicação que o GC não pode limpar da memória.

Quando o GC não pode limpar da memória os objetos não utilizados, nós estamos com problemas. A unidade de memória que mantém os objetos não utilizados será ocupada até o fim da aplicação ou (até o fim do método).

até o fim do método? Sim, é isso mesmo. Temos dois tipos de vazamentos, vazamentos que ocupam a unidade de memória até o final da aplicação e vazamentos que ocupam a unidade de memória até o final do método. O primeiro é claro. O segundo precisa de mais esclarecimentos. Vamos tomar um exemplo para explicar isto! Assumamos que temos o método X. O método X está a fazer uma longa tarefa em segundo plano, e vai demorar um minuto a terminar. Além disso, o método X está segurando objetos não utilizados enquanto faz isso. Nesse caso, a unidade de memória será ocupada, e os objetos não utilizados não poderão ser limpos por um minuto até o final da tarefa. Após o término do método, o GC pode limpar os objetos não utilizados e recuperar a memória.

É isso que eu quero que você saiba por enquanto que voltaremos a isso mais tarde com algum código e visualização. SERÁ DIVERTIDO. 👹😜

Espere um minuto!!🥴

Antes de saltar para o contexto, vamos começar com o básico.

>

RAM, ou memória de acesso aleatório, é a memória em dispositivos andróides ou computadores que costumavam armazenar aplicações em execução corrente e seus dados.

>

I’m going to explain two main characters in the RAM, the first is the Heap, and the second is the Stack. Vamos passar para a parte divertida 🤩🍻.

Heap & Stack

>

Não vou demorar muito. Vamos direto ao ponto, uma breve descrição, o Stack é usado para alocação de memória estática enquanto o Heap é usado para alocação dinâmica de memória. Tenha em mente que tanto o Heap como a Pilha são armazenados na RAM.

Mais sobre o Heap Memory

Java heap memory é usado pela máquina virtual para alocação de objetos. Sempre que você cria um objeto, ele é sempre criado no heap. Máquinas virtuais, como JVM ou DVM, fazem a coleta de lixo (GC) regular, tornando a memória heap de todos os objetos que não são mais referenciados disponíveis para alocações futuras.

Para proporcionar uma experiência suave ao usuário, o Android define um limite rígido no tamanho da heap para cada aplicação em execução. O limite de heap size varia entre os dispositivos e é baseado na quantidade de RAM que um dispositivo tem. Se o seu aplicativo atingir este limite de heap e tentar alocar mais memória, ele receberá um OutOfMemoryError e terminará.

Pergunte-se qual é o tamanho do heap para o seu aplicativo?

Vamos descobrir isso juntos. Em andróide, temos o Dalvik VM (DVM). O DVM é uma máquina virtual Java única, otimizada para dispositivos móveis. Ele otimiza a máquina virtual para memória, duração da bateria e desempenho, e é responsável por distribuir a quantidade de memória para cada aplicação.

Vamos falar sobre duas linhas no DVM:

  1. dalvik.vm.heapgrowthlimit: Esta linha é baseada em como o Dalvik vai começar no tamanho da pilha da sua aplicação. É o tamanho padrão do heap size para cada aplicação. O máximo que sua aplicação pode alcançar!
  2. dalvik.vm.heapsize: Esta linha representa o tamanho máximo do heap size para um heap maior. Você pode conseguir isso pedindo ao androide para uma pilha maior no manifesto da sua aplicação (android:largeHeap=”true”).

Não use uma pilha maior na sua aplicação. Faça isso SOMENTE se você souber exatamente o efeito colateral deste passo. Aqui eu lhe darei informações suficientes para continuar pesquisando o tópico.

Aqui está uma tabela mostrando qual o tamanho do heap que você tem baseado na RAM do seu dispositivo:

+==========================+=========+=========+===================+
| DVM | 1GB RAM | 2GB RAM | 3GB RAM OR HIGHER |
+==========================+=========+=========+===================+
| DEFAULT(heapgrowthlimit) | 64m | 128m | 256m |
+--------------------------+---------+---------+-------------------+
| LARGE(heapsize) | 128m | 256m | 512m |
+--------------------------+---------+---------+-------------------+

Recorde quanto mais ram você tiver, maior será o tamanho do heap. Tenha em mente que nem todos os dispositivos com maior ram vão acima de 512m faça a sua pesquisa no seu dispositivo se o seu dispositivo tem mais de 3GB para ver se o seu tamanho da pilha é maior que 512m.

Como pode verificar o tamanho da pilha do aplicativo para o seu dispositivo?

Usando o ActivityManager. Você pode verificar o tamanho máximo da pilha em tempo de execução usando os métodos getMemoryClass() ou getLargeMemoryClass() (quando uma pilha grande está ativada).

  • getMemoryClass(): Retorna o tamanho máximo padrão do heap.
  • getLargeMemoryClass(): Retorna o tamanho máximo de pilha disponível depois de ativar a bandeira de pilha grande no manifesto.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Como funciona no mundo real?

Usaremos esta aplicação simples para entender quando usamos a pilha e quando usamos a pilha.

>

aplicação

A imagem abaixo mostra uma representação da pilha e da pilha da aplicação e onde cada objeto aponta e é armazenado quando executamos a aplicação.

>>

Vamos rever a execução da aplicação, paramos cada linha, explicamos quando a aplicação aloca os objectos, e armazenamo-los na pilha ou na pilha. Veremos também quando a aplicação libera os objetos da pilha e do heap.

>

  • Linha 1 – A JVM cria um bloco de memória da pilha para o método principal.

>

  • Linha 2 – Nesta linha, criamos uma variável local primitiva. A variável será criada e armazenada na memória da pilha do método principal.
>

  • Linha 3 – Aqui preciso da vossa atenção!! Nesta linha, nós criamos um novo objeto. O objeto é criado na pilha do método principal e armazenado na pilha. A pilha armazena a referência, o endereço de memória do objeto (ponteiro), enquanto a pilha armazena o objeto original.
  • >

>

>

>

>

>>

>>

>

>

>

    >

  • Linha 4 – O mesmo da linha 3.

  • Linha 5 – A JVM cria um bloco de memória de pilha para o método foo.

  • Linha 6 -Criar um novo objecto. O objeto é criado na memória da pilha do método foo, e nós armazenamos na pilha o endereço da memória da pilha do objeto que criamos na Linha 3. O valor (endereço da memória heap do objeto na linha 3) que passamos na linha 5. Tenha em mente que Java sempre passa referências por valor.

  • Linha 7 – Estamos criando um novo objeto. O objeto criado na pilha e apontado para o conjunto de cordas na pilha.
>

>

  • Linha 8 – Na linha final do método foo, o método terminou. E os objetos serão liberados do bloco de pilha do método foo.

>

  • Linha 9- O mesmo da linha 8, Na linha final do método principal, o método terminou. E o bloco de pilha do método principal torna-se livre.

E quanto a libertar a memória da pilha? Chegaremos lá em breve. Pegue coffee☕️, e continue 😼.

O que acontece quando os métodos são terminados?

Todos os métodos têm escopo próprio. Quando o método é terminado, os objetos são liberados e recuperados automaticamente da pilha.

figurar 1

Na Figura 1, quando o método foo terminou. A memória da pilha ou o bloco de pilha do método foo será liberado e recuperado automaticamente.

figurar 2

>

Na Figura 2, O mesmo. Quando o método main terminou. A memória da pilha ou o bloco de pilha do método principal será liberado e recuperado automaticamente.

Conclusão

Agora, está claro para nós que os objetos na pilha são temporários. Quando o método terminar, os objetos serão liberados e recuperados.

A pilha é uma estrutura de dados LIFO (Last-In-First-Out). Você pode vê-la como uma caixa. Usando esta estrutura, o programa pode facilmente gerenciar todas as suas operações usando duas operações simples: push e pop.

Tudo o tempo que você precisar para salvar algo como uma variável ou método ele empurra e move o ponteiro da pilha para cima. Cada vez que você sai de um método, ele coloca tudo desde o ponteiro da pilha até o retorno ao endereço do método anterior. No nosso exemplo voltando do método foo para o método principal.

E quanto ao Heap?

O heap é diferente da pilha. Para liberar e recuperar os objetos da memória heap, precisamos de ajuda.

Para isso, Java, ou para ser mais preciso, a JVM fez um super-herói para nos ajudar. Nós o chamamos de Garbage Collector. Ele vai fazer o trabalho duro por nós. E se preocupando em detectar objetos não utilizados, liberá-los e recuperar mais espaço na memória.

Como funciona o coletor de lixo?

Simples. O coletor de lixo está procurando por objetos inutilizados ou inacessíveis. Quando há um objeto na pilha que não tem nenhuma referência apontada para ele, o lixeiro se encarrega de liberá-lo da memória e recuperar mais espaço.

>

>

>

>

>

>

GC

>

Raízes GC são objetos referenciados pela JVM. Elas são os objetos iniciais da árvore. Cada objeto na árvore tem um ou mais objetos raiz. Desde que a aplicação ou as raízes GC possam alcançar essas raízes ou esses objetos, a árvore inteira é alcançável. Uma vez que eles se tornem inalcançáveis a partir do aplicativo ou das raízes de GC, eles serão considerados como objetos não utilizados ou inalcançáveis.

O que acontece quando o coletor de lixo é executado?

Por enquanto, este é o estado atual da memória do aplicativo. A pilha está limpa, e a pilha está cheia de objetos não utilizados.

Antes da GC

Após a execução da GC os resultados serão os seguintes:

Após GC

O GC irá libertar e limpar todos os objectos não utilizados da pilha.

>

Homem! E a fuga de memória de que estamos à espera? LOL, só um pouco mais de Lil, e nós estaremos lá. Na nossa aplicação simples, a aplicação foi escrita de forma muito simples. Não havia nada de errado com o código que pudesse impedir o GC de liberar os objetos do heap. E por essa razão, o GC libera e recupera todos os objetos da memória do heap. Continua. Temos muitos exemplos de vazamentos de memória no próximo section😃.

Quando e como acontecem vazamentos de memória?

Um vazamento de memória acontece quando a pilha ainda faz referência a objetos não utilizados na pilha.

Existe uma representação visual simples na imagem abaixo para uma melhor compreensão do conceito.

>

Na representação visual, vemos que quando temos objectos referenciados da pilha mas que já não estão em uso. O coletor de lixo nunca irá liberá-los ou libertá-los da memória porque mostra que esses objetos estão em uso enquanto eles não estão.

Como podemos causar um vazamento?

Há várias maneiras de causar um vazamento de memória no Android. E pode ser feito facilmente usando AsyncTasks, Handlers, Singleton, Threads, e mais.

Mostrarei alguns exemplos usando threads, singleton, e listeners para explicar como podemos causar um vazamento e evitá-los e corrigi-los.

Verifica o meu repositório Github. Eu tenho alguns exemplos de código.

Como podemos causar um vazamento usando threads?

Neste exemplo, vamos iniciar uma atividade que executa uma thread em segundo plano. A thread vai fazer uma tarefa que leva 20 segundos para terminar.

Como é sabido, as classes internas têm uma referência implícita à sua classe envolvente.

Atrás das cenas, esta é realmente a aparência da atividade.

A DownloadTask contém uma referência da ThreadActivity.

O que acontece depois de iniciar a tarefa ou a thread?

Existem dois fluxos possíveis para a utilização da aplicação. Um fluxo regular que funciona como esperado sem erros e um fluxo de fuga que causa uma fuga de memória.

Fluxo regular

Na figura, estamos apresentando a pilha e a pilha da aplicação.

O usuário inicia a aplicação, abre o ThreadActivity, e espera na tela até o término da tarefa de download. O usuário esperou por um período de 20 segundos. Por que 20 segundos? Porque este é o tempo que a thread leva para completar a tarefa.

>

Task Running

A tarefa está sendo executada em segundo plano. O usuário esperou por 20 segundos para a conclusão da tarefa de download. Quando a tarefa está pronta, a pilha libera o bloco do método run().

>

>

>

Não há referência que contenha a DownloadTask. O GC considerou o objeto DownladTask como um objeto não usado, e por essa razão, o próximo ciclo de GC irá limpá-lo da memória da pilha.

>

>

>

>

>

O GC limpa os objetos não usados da pilha. Agora, quando o usuário fecha a atividade. O método principal será liberado da pilha, e no próximo ciclo do GC, o GC limpará a ThreadActivity da memória da pilha.

>

>

>

>

>

>

Perfeito!