The Android Developer Challenge is back! Submit your idea before December 2.

Ver as alocações de heap e memória do Java 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 aplicativo. O componente mostra um gráfico em tempo real do uso da memória pelo aplicativo, permite capturar um despejo de heap, 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 aplicativo para o qual 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, verifique 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 aplicativo 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 aplicativo

O Android oferece um ambiente de memória gerenciado: quando determina que o aplicativo 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 é constantemente aprimorada. 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 aplicativo alocar memória com mais rapidez que o sistema consegue coletá-la, o aplicativo poderá ficar mais lento enquanto o coletor libera memória suficiente para atender às alocações. Esse atraso pode fazer com que o aplicativo pule quadros e gere uma lentidão visível.

Mesmo quando não exibe essa lentidão, se o aplicativo vazar memória, poderá reter essa memória mesmo quando estiver em segundo plano. Esse comportamento pode diminuir o resto do desempenho de memória do sistema, forçando 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 indesejáveis 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 está alocando um número excessivo de objetos em um curto período ou alocando objetos que vazam.

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

Visão geral do Memory Profiler

Quando o Memory Profiler for aberto pela primeira vez, você verá uma linha do tempo detalhada do uso de memória do aplicativo e poderá acessar ferramentas para forçar a coleta de lixo, capturar um despejo de heap 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 despejo de heap 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 o desempenho do aplicativo 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 da memória, que inclui o seguinte:
    • Um gráfico de barras empilhadas, mostrando quanta memória está sendo usada em cada categoria de memória, indicada 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 posteriores, a criação do perfil avançada está sempre ativada para aplicativos depuráveis.

Como a memória é contada

Os números exibidos na parte superior do Memory Profiler (figura 2) são baseados em todas as páginas de memória privada confirmadas pelo aplicativo, de acordo com o sistema Android. Essa contagem não inclui páginas compartilhadas com o sistema ou com outros aplicativos.

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 usar C++ no aplicativo, poderá ver alguma memória nativa usada aqui porque a estrutura Android usa memória nativa para processar várias tarefas em seu nome, como o processamento de ativos de imagem e outros gráficos, mesmo que o código tenha sido escrito em Java ou Kotlin.

  • Graphics: memória 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 aplicativo. Normalmente, esse uso é relacionado ao número de encadeamentos executado pelo aplicativo.

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

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

  • Allocated: o número de objetos Java/Kotlin alocados pelo aplicativo. Não estão incluídos objetos alocados em C ou C++.

    Quando conectado a um dispositivo com Android 7.1 ou versões anteriores, essa contagem de alocação inicia somente no momento em que o Memory Profiler se conecta ao aplicativo em execução. Portanto, qualquer objeto alocado antes do início da criação do perfil não é contado. 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 aplicativo no Android 8.0 ou versões posteriores.

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" deve 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 aplicativo desde a ramificação do Zygote. Portanto, é uma representação precisa da quantidade de memória física realmente utilizada pelo aplicativo.

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 o encadeamento onde a alocação foi efetuada.
  • Quando os objetos foram desalocados (somente ao usar um dispositivo com Android 8.0 ou posterior).

Se o dispositivo estiver executando o Android 8.0 ou versões posteriores, você poderá ver as alocações de objetos a qualquer momento da seguinte maneira: basta clicar na linha do tempo e arrastar para selecionar a região em que você quer ver as alocações (como mostrado no vídeo 1). Não é necessário iniciar uma sessão de gravação porque o Android 8.0 ou posterior inclui uma ferramenta de criação do perfil no dispositivo que rastreia constantemente as alocações dos aplicativos.

Vídeo 1. Com o Android 8.0 e versões posteriores, selecione uma área existente da linha do tempo para ver alocações de objetos

Se o dispositivo estiver executando Android 7.1 ou anterior, clique em Record memory allocations na barra de ferramentas do Memory Profiler. Durante a gravação, o Memory Profiler rastreia todas as alocações que ocorrem no aplicativo. Quando terminar, clique em Stop recording (o mesmo botão, veja o vídeo 2) para ver as alocações.

Vídeo 2. Com o Android 7.1 ou anterior, você precisa gravar explicitamente as alocações de memória

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 anterior), a lista de objetos alocados é exibida abaixo da linha do tempo, 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 alfabeticamente. 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 clicando em Filter ou pressionando Control + F (Command + F no Mac) e inserindo 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 encadeamento 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 a inicialização. Essas alocações nunca são movidas ou eliminadas.
  • zygote heap: a heap copy-on-write em que um processo do aplicativo é ramificado do sistema Android.
  • app heap: a heap principal na qual o aplicativo aloca memória.
  • JNI heap: a heap que mostra onde as referências da 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 o desempenho do aplicativo 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 posterior, você pode alterar esse comportamento usando 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ê tiver um aplicativo que aloque muitos objetos, poderá observar lentidões visíveis no aplicativo 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 aplicativo 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 aplicativo.

Ver referências globais de JNI

Java Native Interface (JNI) é uma estrutura 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 escolha 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 as alocações de memória para o código JNI do aplicativo, é necessário implantar seu aplicativo em um dispositivo com o Android 8.0 ou posterior.

Para mais informações sobre JNI, consulte as dicas de JNI.

Capturar um despejo de heap

Um despejo de heap mostra quais objetos do aplicativo estão usando memória no momento da captura do despejo de heap. Especialmente após uma sessão de usuário prolongada, um despejo de heap 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 despejo de heap somente com o Android 7.1 ou versões anteriores, quando você captura o despejo de memória durante a gravação de alocações.

Para capturar um despejo de heap, clique em Dump Java heap na barra de ferramentas do Memory Profiler. 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 aplicativo e precisa de memória para coletar os dados.

O despejo de heap aparece abaixo da linha do tempo da memória, mostrando todos os tipos de classe na heap, como mostrado na figura 5.

Figura 5. Visualização do despejo de heap

Se for necessária maior precisão para o momento de criação do despejo, você poderá criar um despejo de heap no ponto crítico no código do aplicativo 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 posteriores.

    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 despejo de heap 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 aplicativo aloca memória.
  • image heap: a imagem de inicialização do sistema, que contém as classes pré-carregadas durante a inicialização. Essas alocações nunca são movidas ou eliminadas.
  • zygote heap: a heap copy-on-write em que um processo do aplicativo é 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 despejo de heap 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 classificar 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 posteriores.
  • 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 despejo de heap é 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 alfabeticamente. 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 clicando em Filter ou pressionando Control + F (Command + F no Mac) e inserindo 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 despejo de heap, mostrando os próprios dados de instância.

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

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

Salvar despejo de heap como arquivo HPROF

Após capturar um despejo de heap, 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 despejo de heap é descartado. Portanto, se você quiser salvá-lo para revisão posterior, exporte o despejo de heap para um arquivo HPROF. No Android Studio 3.1 e versões anteriores, o botão Export capture to file fica do lado esquerdo da barra de ferramentas abaixo da linha do tempo. No Android Studio 3.2 e versões posteriores, 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 onde você quer gravar o arquivo HPROF convertido. Por exemplo:

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

Importar um arquivo de despejo de heap

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.

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

Ao usar o Memory Profiler, você precisa estressar o código do aplicativo e tentar forçar vazamentos de memória. Uma forma de provocar vazamentos de memória no aplicativo é deixar que ele execute 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 será o tempo de execução do aplicativo 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 de 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 o aplicativo vaze um objeto Activity, Context ou View porque o sistema recria a Activity e, se o aplicativo mantiver uma referência a um desses objetos em algum lugar, o sistema não conseguirá eliminá-lo por meio da coleta de lixo.
  • Alterne entre seu aplicativo e outro aplicativo em diferentes estados de atividade (navegue até a tela inicial e retorne ao aplicativo).

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