Inspecionar o uso de memória do seu app com o Memory Profiler

O Memory Profiler é um componente do Android Profiler que ajuda a identificar vazamentos e rotatividade de memória, que podem gerar oscilações, travamentos e até falhas no app. O componente mostra um gráfico em tempo real do uso da memória pelo app, permite capturar um heap dump, força coletas de lixo e rastreia alocações de memória.

Para abrir o Memory Profiler, siga estas etapas:

  1. Clique em View > Tool Windows > Profiler (você também pode clicar em Profile na barra de ferramentas).
  2. Selecione o dispositivo e o processo do app para que você quer criar o perfil na barra de ferramentas do Android Profiler. Se você estiver conectado a um dispositivo por USB, mas o dispositivo não for exibido na lista, confira se a depuração USB foi ativada.
  3. Clique em qualquer lugar na linha do tempo MEMORY para abrir o Memory Profiler.

Como alternativa, é possível inspecionar a memória do app na linha de comando com dumpsys, bem como ver eventos de GC no logcat.

Por que é necessário criar o perfil da memória do app

O Android oferece um ambiente de memória gerenciado: quando determina que o app deixou de usar alguns objetos, o coletor de lixo libera a memória não utilizada para a heap. A forma como o Android encontra memória não utilizada é melhorada constantemente. No entanto, em todas as versões do Android, o sistema precisa interromper brevemente seu código em algum momento. Geralmente, as interrupções são imperceptíveis. No entanto, se o app alocar memória com mais rapidez do que o sistema consegue coletá-la, o app pode ficar mais lento enquanto o coletor libera memória suficiente para atender às alocações. Esse atraso pode fazer com que o app pule frames e gere uma lentidão visível.

Ainda que não exiba essa lentidão, se o app vazar memória, pode reter essa memória mesmo quando estiver em segundo plano. Esse comportamento pode diminuir o resto da performance de memória do sistema ao forçar eventos desnecessários de coleta de lixo. Com o tempo, o sistema será forçado a encerrar o processo do aplicativo para recuperar a memória. Se isso ocorrer, quando o usuário retornar ao aplicativo, precisará reiniciá-lo completamente.

Para evitar esses problemas, use o Memory Profiler para fazer o seguinte:

  • Procurar padrões de alocação de memória inadequados na linha do tempo que poderiam causar problemas de desempenho.
  • Despejar a heap Java para ver quais objetos estão consumindo a memória em determinado momento. Vários despejos de heap durante um período de tempo longo podem ajudar a identificar vazamentos de memória.
  • Gravar alocações de memória durante interações normais e extremas do usuário para identificar exatamente onde o código aloca um número excessivo de objetos em um curto período ou aloca objetos que vazam.

Para saber mais sobre práticas de programação que podem reduzir o uso de memória do app, leia Gerenciar a memória do app.

Visão geral do Memory Profiler

Quando o Memory Profiler é aberto pela primeira vez, você vê uma linha do tempo detalhada do uso de memória do app e pode acessar ferramentas para forçar a coleta de lixo, capturar um heap dump e gravar alocações de memória.

Figura 1. Memory Profiler

Como indicado na Figura 1, a visualização padrão do Memory Profiler inclui o seguinte:

  1. Um botão para forçar um evento de coleta de lixo.
  2. Um botão para capturar um despejo de heap.

    Observação: um botão para gravar alocações de memória aparece à direita do botão de heap dump somente quando conectado a um dispositivo com o Android 7.1 (nível de API 25) ou anterior.

  3. Um menu suspenso para especificar com que frequência o criador de perfil captura as alocações de memória. Ao selecionar a opção adequada, isso pode ajudar a melhorar a performance do app durante a criação de perfil.
  4. Botões para aumentar e diminuir o zoom da linha do tempo.
  5. Um botão para acessar os dados da memória ao vivo.
  6. A linha do tempo de eventos, que mostra estados de atividade, eventos de entrada do usuário e eventos de rotação de tela.
  7. A linha do tempo do uso da memória, que inclui o seguinte:
    • Um gráfico empilhado mostrando quanta memória está sendo usada em cada categoria de memória, como indicado pelo eixo y à esquerda e pela legenda de cores na parte superior.
    • Uma linha tracejada indica o número de objetos alocados, indicado pelo eixo y à direita.
    • Um ícone para cada evento de coleta de lixo.

No entanto, se você usar um dispositivo com Android 7.1 ou versões anteriores, nem todos os dados de criação do perfil estarão visíveis por padrão. Se for exibida a mensagem "A criação do perfil avançada não está disponível para o processo selecionado", será preciso ativar a criação do perfil avançada para ver o seguinte:

  • Linha do tempo de eventos
  • Número de objetos alocados
  • Eventos de coleta de lixo

No Android 8.0 ou versões mais recentes, a criação de perfil avançada está sempre ativada para apps depuráveis.

Como a memória é contada

Os números exibidos na parte superior do Memory Profiler (Figura 2) têm como base todas as páginas de memória particular confirmadas pelo app, de acordo com o sistema Android. Essa contagem não inclui páginas compartilhadas com o sistema ou com outros apps.

Figura 2. Legenda da contagem de memória na parte superior do Memory Profiler

As categorias na contagem de memória são as seguintes:

  • Java: memória de objetos alocados em código Java ou Kotlin.
  • Native: memória de objetos alocados em código C ou C++.

    Mesmo que você não use C++ no app, pode ver alguma memória nativa usada aqui, porque o framework do Android usa memória nativa para processar várias tarefas em seu nome, como o processamento de recursos de imagem e outros gráficos, mesmo que o código tenha sido escrito em Java ou Kotlin.

  • Memória gráfica: usada para filas do buffer de gráficos para exibir pixels na tela, incluindo superfícies GL, texturas GL e assim por diante. Observe que essa é uma memória compartilhada com a CPU e não a memória dedicada da GPU.

  • Stack: memória usada pelas pilhas nativa e Java no app. Geralmente, isso está relacionado a quantas linhas de execução seu app está executando.

  • Code: memória usada pelo app para código e recursos, como bytecode dex, código dex otimizado ou compilado, bibliotecas .so e fontes.

  • Other: memória usada pelo app que o sistema não sabe como categorizar.

  • Allocated: o número de objetos Java/Kotlin alocados pelo app, sem incluir objetos alocados em C ou C ++.

    Quando conectada a um dispositivo com Android 7.1 ou versões anteriores, essa contagem de alocação começa somente no momento em que o Memory Profiler se conecta ao app em execução. Portanto, os objetos alocados antes de você iniciar a criação de perfil não são considerados. No entanto, o Android 8.0 inclui uma ferramenta de criação do perfil no dispositivo que rastreia todas as alocações. Portanto, esse número sempre representa o número total de objetos Java pendentes no app no Android 8.0 ou versões mais recentes.

Quando comparado às contagens de memória da ferramenta Android Monitor anterior, o novo Memory Profiler registra a memória de forma diferente. Portanto, pode parecer que agora o uso da memória aumentou. O Memory Profiler monitora algumas categorias extras que aumentam o total. Se você se preocupa apenas com a memória heap Java, o número "Java" precisa ser semelhante ao valor da ferramenta anterior. E, embora esse número Java não corresponda exatamente ao que você viu no Android Monitor, o número inclui todas as páginas de memória física alocadas à heap Java do app desde a ramificação do Zygote. Portanto, é uma representação precisa da quantidade de memória física realmente usada pelo app.

Ver alocações de memória

Alocações de memória mostram como cada objeto Java e referência JNI na memória foram alocados. Especificamente, o Memory Profiler pode mostrar as seguintes informações sobre as alocações de objeto:

  • Que tipos de objetos foram alocados e quanto espaço eles usam.
  • O rastreamento de pilha de cada alocação, incluindo a linha de execução onde a alocação foi efetuada.
  • Quando os objetos foram desalocados (apenas ao usar um dispositivo com Android 8.0 ou mais recentes).

Para gravar alocações Java e Kotlin, selecione Record Java / Kotlin allocations e escolha Record. Se o dispositivo está executando o Android 8 ou versões mais recentes, a interface do Memory Profiler faz a transição para uma tela separada que exibe a gravação em andamento. É possível interagir com a mini linha do tempo acima da gravação (por exemplo, para mudar o intervalo de seleção). Para concluir a gravação, selecione Stop .

Visualização de alocações Java no Memory Profiler

No Android 7.1 e versões anteriores, o Memory Profiler usa a gravação de alocação legada, que exibe a gravação na linha do tempo até você clicar em Stop.

Após selecionar uma parte da linha do tempo, ou após concluir uma sessão de gravação com um dispositivo executando Android 7.1 ou versões anteriores, a lista de objetos alocados aparece agrupada por nome da classe e classificada pela contagem de heap.

Para inspecionar o registro de alocação, siga estas etapas:

  1. Procure na lista objetos com contagens de heap anormalmente altas e que podem conter vazamentos. Para ajudar a encontrar as classes conhecidas, clique no cabeçalho da coluna Class Name para classificar em ordem alfabética. Em seguida, clique em um nome de classe. O painel Instance View é exibido à direita, mostrando cada instância dessa classe, como mostrado na Figura 3.
    • Como alternativa, você pode localizar objetos rapidamente ao clicar em Filter ou ao pressionar Control + F (Command + F no Mac) e inserir um nome de classe ou pacote no campo de pesquisa. Também é possível pesquisar pelo nome do método selecionando Arrange by callstack no menu suspenso. Se você quiser usar expressões regulares, marque a caixa ao lado de Regex. Marque a caixa ao lado de Match case para que sua consulta de pesquisa diferencie maiúsculas de minúsculas.
  2. No painel Instance View, clique em uma instância. A guia Call Stack é exibida abaixo, mostrando onde e em que a linha de execução essa instância foi alocada.
  3. Na guia Call Stack, clique com o botão direito do mouse em qualquer linha e selecione Jump to Source para abrir esse código no editor.

Figura 3. Detalhes sobre cada objeto alocado aparecem na Instance View, à direita

Você pode usar os dois menus acima da lista de objetos alocados para escolher qual heap inspecionar e como organizar os dados.

No menu à esquerda, escolha qual heap precisa ser inspecionada:

  • default heap: quando nenhuma heap é especificada pelo sistema.
  • image heap: a imagem de inicialização do sistema, que contém as classes pré-carregadas durante o tempo de inicialização. Essas alocações nunca são movidas ou eliminadas.
  • zygote heap: a heap copy-on-write em que um processo do app é ramificado do sistema Android.
  • app heap: a heap principal na qual o app aloca memória.
  • JNI heap: a heap que mostra onde as referências da Java Native Interface (JNI) são alocadas e liberadas.

No menu à direita, escolha como organizar as alocações:

  • Arrange by class: agrupa as alocações de acordo com o nome da classe. Esse é o padrão.
  • Arrange by package: agrupa as alocações com base no nome do pacote.
  • Arrange by callstack: agrupa as alocações por pilha de chamadas correspondente.

Melhorar o desempenho do aplicativo durante a criação de perfil

Para melhorar a performance do app durante a criação de perfil, o Memory Profiler analisa as alocações de memória periodicamente por padrão. Ao testar dispositivos com nível de API 26 ou mais recentes, você pode mudar esse comportamento com o menu suspenso Allocation Tracking. As opções disponíveis são as seguintes:

  • Full: captura todas as alocações de objeto na memória. Esse é o comportamento padrão no Android Studio 3.2 e versões anteriores. Se você tem um app que aloca muitos objetos, pode observar lentidões durante a criação de perfil.
  • Sampled: cria amostras de alocações de objetos na memória em intervalos regulares. Essa é a opção padrão e tem menos impacto no desempenho do app durante a criação de perfil. Apps que alocam muitos objetos em um curto espaço de tempo ainda podem exibir uma lentidão visível.
  • Off: interrompe o rastreamento da alocação de memória do app.

Ver referências globais de JNI

Java Native Interface (JNI) é um framework que permite que o código Java e o código nativo chamem um ao outro.

As referências de JNI são gerenciadas manualmente pelo código nativo, portanto, é possível que objetos Java usados pelo código nativo sejam mantidos ativos por muito tempo. Alguns objetos na heap Java podem ficar inacessíveis se uma referência de JNI for descartada sem primeiro ser explicitamente excluída. Além disso, é possível esgotar o limite de referências globais de JNI.

Para solucionar esses problemas, use a visualização JNI heap no Memory Profiler para procurar todas as referências de JNI globais e filtrá-las por tipos de Java e pilhas de chamadas nativas. Com essas informações, é possível encontrar quando e onde as referências de JNI globais são criadas e excluídas.

Enquanto seu app está em execução, selecione uma parte da linha do tempo que você quer inspecionar e selecione JNI heap no menu suspenso acima da lista de classes. Você pode inspecionar objetos na heap como faria normalmente e clicar duas vezes nos objetos na guia Allocation Call Stack para ver onde as referências de JNI são alocadas e liberadas no código, como mostrado na Figura 4.

Figura 4. Visualização de referências globais de JNI

Para inspecionar alocações de memória para o código JNI do app, é necessário implantar seu app em um dispositivo com o Android 8.0 ou versão mais recente.

Para saber mais sobre JNI, consulte as dicas de JNI.

Memory Profiler nativo

O Memory Profiler do Android Studio inclui um Native Memory Profiler para apps implantados em dispositivos físicos e virtuais com o Android 10 e versões mais recentes.

O Native Memory Profiler rastreia as alocações/desalocações de objetos no código nativo em um período específico e fornece as seguintes informações:

  • Allocations: uma contagem de objetos alocados por malloc() ou pelo operador new durante o período selecionado.
  • Deallocations: uma contagem de objetos desalocados por free() ou pelo operador delete durante o período selecionado.
  • Deallocations: o tamanho agregado em bytes de todas as alocações durante o período selecionado.
  • Deallocations Size: o tamanho agregado em bytes de toda a memória liberada durante o período selecionado.
  • Total Count: o valor da coluna Allocations menos o valor da coluna Deallocations.
  • Remaining Size: o valor da coluna Allocations Size menos o valor na coluna Deallocations Size.

Memory Profiler nativo

Para gravar alocações nativas em dispositivos com Android 10 e versões mais recentes, selecione Record native allocations e Record. A gravação continua até você clicar em Stop . Depois disso, a IU do Memory Profiler faz a transição para uma tela separada que mostra a gravação nativa.

Botão "Record native allocations"

No Android 9 e versões anteriores, a opção Record native allocations não está disponível.

Por padrão, o Native Memory Profiler usa o tamanho de amostra de 32 bytes: sempre que 32 bytes de memória são alocados, um instantâneo da memória é tirado. Uma amostra menor resulta em snapshots mais frequentes, gerando dados mais precisos sobre o uso da memória. Um tamanho de amostra maior produz dados menos precisos, mas consome menos recursos no sistema e melhora o desempenho durante a gravação.

Para mudar o tamanho da amostra do Native Memory Profiler:

  1. Selecione Run > Edit Configurations.
  2. 2. Selecione o módulo de aplicativo no painel esquerdo.
  3. Clique na guia Profiling e insira o tamanho da amostra no campo rotulado Native memory sampling interval (bytes).
  4. Compile e execute seu aplicativo novamente.

Capturar um despejo de heap

Um heap dump mostra quais objetos do app estão usando memória no momento da captura do heap dump. Especialmente após uma sessão de usuário prolongada, um heap dump pode ajudar a identificar vazamentos de memória, mostrando objetos que ainda estão na memória e que você acredita que já deveriam ter sido removidos dela.

Após capturar um despejo de heap, você pode ver o seguinte:

  • Quais tipos de objetos e quantos objetos de cada tipo foram alocados pelo aplicativo.
  • Quanta memória está sendo usada em cada objeto.
  • Em que local do código estão sendo mantidas referências a cada objeto.
  • A pilha de chamadas de onde um objeto foi alocado. No momento, as pilhas de chamadas estão disponíveis em um heap dump apenas com o Android 7.1 e versões anteriores, quando você captura o heap dump durante a gravação de alocações.

Para capturar um heap dump, selecione Capture heap dump e escolha Record. Durante o despejo de heap, a quantidade de memória do Java pode aumentar temporariamente. Isso é normal porque o despejo de heap ocorre no mesmo processo do app e precisa de memória para coletar os dados.

Depois que o criador de perfil termina a captura do heap dump, a IU do Memory Profiler faz a transição para uma tela separada que vai mostrar o heap dump.

Figura 5. Visualização do heap dump.

Se for necessária maior precisão para o momento de criação do despejo, você pode criar um heap dump no ponto crítico no código do app, chamando dumpHprofData().

Na lista de classes, você pode ver as seguintes informações:

  • Allocations: número de alocações na heap.
  • Native Size: quantidade total de memória nativa usada por esse tipo de objeto (em bytes). Essa coluna é visível apenas para o Android 7.0 e versões mais recentes.

    Você verá memória aqui para alguns objetos alocados em Java porque o Android usa memória nativa para algumas classes de estrutura, como Bitmap.

  • Shallow Size: quantidade total de memória Java usada por esse tipo de objeto (em bytes).

  • Retained Size: tamanho total da memória que está sendo retida devido a todas as instâncias dessa classe (em bytes).

Você pode usar os dois menus acima da lista de objetos alocados para escolher qual heap dump inspecionar e como organizar os dados.

No menu à esquerda, escolha qual heap precisa ser inspecionada:

  • default heap: quando nenhuma heap é especificada pelo sistema.
  • app heap: a heap principal na qual o app aloca memória.
  • image heap: a imagem de inicialização do sistema, que contém as classes pré-carregadas durante o tempo de inicialização. Essas alocações nunca são movidas ou eliminadas.
  • zygote heap: a heap copy-on-write em que um processo do app é ramificado do sistema Android.

No menu à direita, escolha como organizar as alocações:

  • Arrange by class: agrupa as alocações de acordo com o nome da classe. Esse é o padrão.
  • Arrange by package: agrupa as alocações com base no nome do pacote.
  • Arrange by callstack: agrupa as alocações por pilha de chamadas correspondente. Essa opção funciona apenas se você capturar o heap dump durante a gravação de alocações. Mesmo assim, é provável que existam objetos na heap alocados antes do início da gravação. Portanto, essas alocações aparecem primeiro, listadas simplesmente por nome de classe.

Por padrão, a lista é classificada pela coluna Retained Size. Para ordenar pelos valores em uma coluna diferente, clique no cabeçalho da coluna.

Clique no nome de uma classe para abrir a janela Instance View à direita, mostrada na figura 6. Cada instância listada inclui:

  • Depth: o menor número de saltos de qualquer raiz de GC até a instância selecionada.
  • Native Size: o tamanho da instância na memória nativa. Essa coluna é visível apenas para o Android 7.0 e versões mais recentes.
  • Shallow Size: o tamanho da instância na memória Java.
  • Retained Size: o tamanho da memória dominada por essa instância, de acordo com a árvore dominante.

Figura 6. A duração necessária para capturar um heap dump é indicada na linha do tempo

Para inspecionar a heap, siga estas etapas:

  1. Procure na lista objetos com contagens de heap anormalmente altas e que podem conter vazamentos. Para ajudar a encontrar as classes conhecidas, clique no cabeçalho da coluna Class Name para classificar em ordem alfabética. Em seguida, clique em um nome de classe. O painel Instance View é exibido à direita, mostrando cada instância dessa classe, como mostrado na Figura 6.
    • Como alternativa, você pode localizar objetos rapidamente ao clicar em Filter ou ao pressionar Control + F (Command + F no Mac) e inserir um nome de classe ou pacote no campo de pesquisa. Também é possível pesquisar pelo nome do método selecionando Arrange by callstack no menu suspenso. Se você quiser usar expressões regulares, marque a caixa ao lado de Regex. Marque a caixa ao lado de Match case para que sua consulta de pesquisa diferencie maiúsculas de minúsculas.
  2. No painel Instance View, clique em uma instância. A guia References aparece abaixo, mostrando cada referência a esse objeto.

    Também é possível clicar na seta ao lado do nome da instância para ver todos os campos dela e clicar em um nome de campo para ver todas as referências dele. Se você quiser ver os detalhes da instância para um campo, clique com o botão direito do mouse no campo e selecione Go to Instance.

  3. Na guia References, se você identificar uma referência que pode estar vazando memória, clique com o botão direito nela e selecione Go to Instance. A instância correspondente será selecionada no heap dump, mostrando os próprios dados de instância.

No heap dump, procure vazamentos de memória causados por:

  • Referências prolongadas a Activity, Context, View, Drawable e outros objetos que podem manter uma referência ao contêiner Activity ou Context.
  • Classes internas não estáticas como Runnable, que podem manter uma instância Activity.
  • Caches que armazenar objetos por mais tempo do que o necessário.

Salvar heap dump como arquivo HPROF

Após capturar um heap dump, os dados podem ser vistos no Memory Profiler apenas durante a execução do criador de perfil. Quando você encerra a sessão de criação do perfil, o heap dump é descartado. Portanto, se quiser salvá-lo para revisão posterior, exporte o heap dump para um arquivo HPROF. No Android Studio 3.1 e versões anteriores, o botão Export capture to file fica no lado esquerdo da barra de ferramentas, abaixo da linha do tempo. No Android Studio 3.2 e versões mais recentes, há um botão Export Heap Dump à direita de cada entrada Heap Dump no painel Sessions. Na caixa de diálogo Export As que é exibida, salve o arquivo com a extensão de nome de arquivo .hprof.

Para usar um analisador de HPROF diferente, como jhat, é preciso converter o arquivo HPROF do formato Android para o formato HPROF do Java SE. Você pode fazer isso com a ferramenta hprof-conv fornecida no diretório android_sdk/platform-tools/. Execute o comando hprof-conv com dois argumentos: o arquivo HPROF original e o local para gravar o arquivo HPROF convertido. Por exemplo:

hprof-conv heap-original.hprof heap-converted.hprof

Importar um arquivo heap dump

Para importar um arquivo HPROF (.hprof), clique em Start a new profiling session no painel Sessions, selecione Load from file e escolha o arquivo no navegador.

Você também pode importar um arquivo HPROF arrastando-o do navegador de arquivos para uma janela do editor.

Detecção de vazamento no Memory Profiler

Ao analisar um heap dump no Memory Profiler, agora é possível filtrar dados de criação de perfil que o Android Studio considera como vazamentos de memória em instâncias de Activity e Fragment do seu app.

Os tipos de dados mostrados pelo filtro incluem o seguinte:

  • Instâncias de Activity que foram destruídas, mas ainda estão sendo referenciadas.
  • Instâncias de Fragment que não têm um FragmentManager, mas ainda estão sendo referenciadas.

Em situações como as descritas a seguir, o filtro pode gerar falsos positivos.

  • Um Fragment foi criado, mas ainda não foi usado.
  • Um Fragment está sendo armazenado em cache, mas não como parte de um FragmentTransaction.

Para usar esse recurso, primeiro capture um heap dump ou importe um arquivo de heap dump para o Android Studio. Para exibir os fragmentos e as atividades que podem estar vazando memória, marque a caixa de seleção Activity/Fragment Leaks no painel de despejo de heap do Memory Profiler, como mostrado na Figura 7.

Profiler: detecção de vazamento de memória

Figura 7. Filtragem de um heap dump em busca de vazamentos de memória.

Técnicas para criação do perfil da memória

Ao usar o Memory Profiler, você precisa estressar o código do app e tentar forçar vazamentos de memória. Uma forma de provocar vazamentos de memória no app é deixar que ele seja executado por algum tempo antes de inspecionar a heap. Os vazamentos podem subir para a parte superior das alocações na heap. No entanto, quanto menor o vazamento, maior o tempo de execução do app necessário para poder vê-lo.

Também é possível usar um dos seguintes métodos para provocar um vazamento de memória:

  • Gire o dispositivo da orientação retrato para paisagem e vice-versa várias vezes em diferentes estados de atividade. Muitas vezes, a rotação do dispositivo pode fazer com que um app vaze um objeto Activity, Context ou View, porque o sistema recria a Activity e, se o app mantiver uma referência a um desses objetos em outro lugar, o sistema não poderá coletá-lo como lixo.
  • Alterne entre seu app e outro app em diferentes estados de atividade, navegue até a tela inicial e retorne ao app.

Dica: você também pode executar as etapas acima usando o framework de teste monkeyrunner.