Register now for Android Dev Summit 2019!

Ver as alocações de pilha 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 pilha, 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 > Android Profiler (você também pode clicar em Android Profiler 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 de 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 você deve 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 pilha. A forma como o Android encontra memória não utilizada é constantemente aprimorada. No entanto, em todas as versões do Android, o sistema deve interromper brevemente o seu código em algum momento. Na maior parte das vezes, 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á ser retardado enquanto o coletor libera memória suficiente para atender às alocações. Esse atraso pode fazer com 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, deverá reiniciá-lo completamente.

Para evitar esses problemas, você deve usar 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 pilha Java para ver quais objetos estão consumindo a memória em um determinado momento. Vários despejos de pilha ao longo de um período longo podem ajudar a identificar vazamentos de memória.
  • Grave 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 obter 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 é aberto pela primeira vez, você verá uma linha do tempo detalhada do uso de memória do aplicativo e acessar ferramentas para forçar a coleta de lixo, capturar um despejo de pilha e gravar alocações de memória.

Figura 1. O 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 pilha.
  3. Um botão para gravar alocações de memória. Esse botão somente será exibido quando o dispositivo conectado executar Android 7.1 ou anterior.
  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 quanto 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 executando Android 7.1 ou anterior, 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 posterior, 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. A 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 se 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, embora o código escrito seja 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 memória é a 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 executando Android 7.1 ou anterior, 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 posterior.

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 da pilha 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 à pilha 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.

Observação: no momento, o Memory Profiler também mostra indevidamente algum uso de memória nativa pelo aplicativo que na verdade é efetuado pelas ferramentas de criação do perfil. Até 10 MB de memória são adicionados para cerca de 100 mil objetos. Em uma versão futura das ferramentas, esses números serão filtrados dos dados.

Ver alocações de memória

As alocações de memória mostram como cada objeto na memória foi alocado. Especificamente, o Memory Profiler pode mostrar as seguintes informações sobre as alocações de objeto:

  • Que tipos de objetos foi alocado 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 eram desalocados (somente ao usar um dispositivo com Android 8.0 ou posterior).

Se o dispositivo estiver executando o Android 8.0 ou posterior, você poderá ver as alocações de objetos a qualquer momento da seguinte forma: 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 há necessidade de 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 ou posterior, selecione uma área de linha do tempo existente 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 Android Monitor rastreia todas as alocações que ocorrem no aplicativo. Para concluir, 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 região na 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 da pilha.

Observação: no Android 7.1 ou anterior, é possível gravar no máximo 65535 alocações. Se a sessão de gravação exceder esse limite, somente as 65535 alocações mais recentes serão salvas no registro. (Não há um limite prático no Android 8.0 ou posterior.)

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

  1. Procure na lista objetos com contagens de pilha 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.
  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 em qualquer linha para acessar seu código no editor.

Figura 3. Os detalhes de cada objeto alocado são exibidos em Instance View, à direita

Por padrão, a lista de alocações à esquerda é organizada por nome da classe. Na parte superior da lista, você pode usar a lista suspensa à direita para alternar entre as seguintes organizações:

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

Capturar um despejo de pilha

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

Figura 4. Visualização do despejo de pilha

Para capturar um despejo de pilha, clique em Dump Java heap na barra de ferramentas do Memory Profiler. Durante o despejo da pilha, a quantidade de memória do Java pode aumentar temporariamente. Isso é normal porque o despejo de pilha ocorre no mesmo processo do aplicativo e precisa de alguma memória para coletar os dados.

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

Observação: se for necessária uma maior precisão para o momento de criação do despejo, você poderá criar um despejo de pilha no ponto crítico no código do aplicativo chamando dumpHprofData().

Para inspecionar a pilha, siga estas etapas:

  1. Procure na lista objetos com contagens de pilha 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 5.
  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 seus campos e clicar em um nome de campo para ver todas as suas referências. E, se você quiser ver os detalhes da instância para um campo, clique com o botão direito 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 pilha, mostrando seus próprios dados de instância.

Por padrão, o despejo de pilha não mostra o rastreamento de pilha para cada objeto alocado. Para obter o rastreamento de pilha, você deve começar a gravar alocações de memória antes de clicar em Dump Java heap. Em seguida, você pode selecionar uma instância em Instance View e ver a guia Call Stack ao lado da guia References, como mostrado na figura 5. No entanto, é provável que alguns objetos tenham sido alocados antes de você começar a gravar alocações. Portanto, a pilha de chamadas não está disponível para esses objetos. As instâncias que não incluem uma pilha de chamadas são indicadas por um badge de pilha no ícone . (Infelizmente, como o rastreamento de pilha exige que você execute a gravação de alocações, no momento não é possível ver o rastreamento de pilha para despejos de pilha no Android 8.0.)

No despejo de pilha, 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 Activity instância.
  • Caches que mantêm objetos por mais tempo que o necessário.

Figura 5. A duração necessária para capturar um despejo de pilha é indicada na linha do tempo

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

  • Heap Count: número de instâncias na pilha.
  • Shallow Size: tamanho total de todas as instâncias nessa pilha (em bytes).
  • Retained Size: tamanho total da memória retida por todas as instâncias dessa classe (em bytes).

Na parte superior da lista de classes, você pode usar a lista suspensa esquerda para alternar entre os seguintes despejos de pilha:

  • Default heap: quando nenhuma pilha é especificada pelo sistema.
  • App heap: a pilha 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 pilha copy-on-write, onde um processo do aplicativo é ramificado do sistema Android.

Por padrão, a lista de objetos da pilha é organizada por nome de classe. Você pode usar a outra lista suspensa para alternar entre as seguintes organizações:

  • Arrange by class: agrupa as alocações de acordo com o nome da classe.
  • Arrange by package: agrupa as alocações de acordo com o 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 pilha durante a gravação de alocações. Mesmo assim, é provável que existam objetos na pilha alocados antes do início da gravação. Portanto, essas alocações aparecem primeiro, listadas simplesmente por nome da classe.

Por padrão, a lista é classificada pela coluna Retained Size. Você pode clicar em qualquer cabeçalho de coluna para mudar a classificação da lista.

Em Instance View, cada instância inclui:

  • Depth: o menor número de saltos de qualquer raiz de GC até a instância selecionada.
  • Shallow Size: tamanho dessa instância.
  • Retained Size: Tamanho da memória dominada por essa instância (de acordo com a árvore dominante).

Salvar o despejo de pilha como HPROF

Após capturar um despejo de pilha, 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 pilha é descartado. Se você quiser salvar o despejo de pilha para análise posterior, exporte-o para um arquivo HPROF clicando em Export heap dump as HPROF file na barra de ferramentas abaixo da linha do tempo. Na caixa de diálogo exibida, não deixe de salvar o arquivo com o sufixo .hprof.

O arquivo pode ser reaberto no Android Studio arrastando-o para uma janela de editor vazia (ou soltando-o na barra de guias de arquivos).

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

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

Ao usar o Memory Profiler, você deve 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 execute por algum tempo antes de inspecionar a pilha. Os vazamentos podem subir para a parte superior das alocações na pilha. No entanto, quanto menor o vazamento, tanto maior o tempo de execução do aplicativo necessário para poder ver esse vazamento.

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

  • Gire o dispositivo 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 outro lugar, o sistema não conseguirá eliminá-lo por meio da coleta de lixo.
  • Alterne entre o 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.