Terminologia de memória

Meggin Kearney
Meggin Kearney

Esta seção descreve termos comuns usados na análise de memória e é aplicável a várias ferramentas de criação de perfil de memória para diferentes idiomas.

Os termos e conceitos descritos aqui se referem ao Profiler de Heap dos Chrome DevTools. Se você já trabalhou com o Java, .NET ou algum outro perfilador de memória, talvez seja necessário relembrar o assunto.

Tamanhos de objetos

Pense na memória como um gráfico com tipos primitivos (como números e strings) e objetos (matrizes associativas). Ela pode ser representada visualmente como um gráfico com vários pontos interconectados, como neste exemplo:

Representação visual da memória.

Um objeto pode armazenar memória de duas maneiras:

  • Diretamente pelo próprio objeto.
  • Implicitamente, mantendo referências a outros objetos e, portanto, impedindo que esses objetos sejam descartados automaticamente por um coletor de lixo (GC).

Ao trabalhar com o Heap Profiler no DevTools, uma ferramenta para investigar problemas de memória encontrados no painel Memória, você provavelmente vai encontrar algumas colunas de informações diferentes. Dois que se destacam são Shallow Size e Retained Size, mas o que eles representam?

Colunas "Shallow" e "Retained Size" no painel "Memória".

Tamanho superficial

Esse é o tamanho da memória que é retido pelo próprio objeto.

Objetos JavaScript típicos têm parte da memória reservada para a descrição e para o armazenamento de valores imediatos. Normalmente, apenas matrizes e strings podem ter um tamanho superficial significativo. No entanto, strings e matrizes externas geralmente têm o armazenamento principal na memória do renderizador, expondo apenas um pequeno objeto wrapper na pilha do JavaScript.

A memória do renderizador é toda a memória do processo em que uma página inspecionada é renderizada: memória nativa + memória de heap JS da página + memória de heap JS de todos os workers dedicados iniciados pela página. No entanto, até mesmo um objeto pequeno pode conter uma grande quantidade de memória indiretamente, impedindo que outros objetos sejam descartados pelo processo de coleta de lixo automático.

Tamanho retido

Esse é o tamanho da memória que é liberada quando o objeto é excluído com os objetos dependentes que foram inacessíveis nas raízes da GC.

As raízes do GC são compostas de identificadores criados (locais ou globais) ao fazer uma referência de código nativo para um objeto JavaScript fora do V8. Todos esses identificadores podem ser encontrados em um snapshot de heap em Raízes do GC > Escopo do identificador e Raízes do GC > Identificadores globais. A descrição dos identificadores nesta documentação sem entrar em detalhes da implementação do navegador pode ser confusa. As raízes do GC e os identificadores não são algo com que você precisa se preocupar.

Há muitas raízes GC internas, e a maioria delas não é interessante para os usuários. Do ponto de vista das aplicações, existem os seguintes tipos de raiz:

  • Objeto global de janela (em cada iframe). Há um campo de distância nos snapshots de heap, que é o número de referências de propriedade no caminho de retenção mais curto da janela.
  • Árvore DOM do documento que consiste em todos os nós DOM nativos acessíveis ao percorrer o documento. Nem todos eles podem ter wrappers JS, mas, se tiverem, os wrappers vão estar ativos enquanto o documento estiver ativo.
  • Às vezes, os objetos podem ser retidos pelo contexto do depurador e pelo console do DevTools (por exemplo, após a avaliação do console). Crie snapshots de heap com console limpo e sem pontos de interrupção ativos no depurador.

O gráfico de memória começa com uma raiz, que pode ser o objeto window do navegador ou o objeto Global de um módulo do Node.js. Você não controla como esse objeto raiz é excluído.

O objeto raiz não pode ser controlado.

O que não pode ser acessado pela raiz recebe GC.

Árvore de retenção de objetos

O heap é uma rede de objetos interconectados. No mundo matemático, essa estrutura é chamada de gráfico ou gráfico de memória. Um gráfico é construído a partir de nós conectados por arestas, ambos com rótulos.

  • Os nós (ou objetos) são rotulados usando o nome da função construtor usada para criá-los.
  • Os arestas são identificadas usando os nomes das propriedades.

Saiba como gravar um perfil usando o Heap Profiler. Algumas das coisas chamativas que podemos ver na gravação do Heap Profiler a seguir incluem a distância: a distância da raiz do GC. Se quase todos os objetos do mesmo tipo estiverem na mesma distância e alguns estiverem em uma distância maior, vale a pena investigar.

Exemplo de distância da raiz.

Dominadores

Os objetos de dominação são compostos por uma estrutura em árvore porque cada objeto tem exatamente um elemento de dominação. Um doador de um objeto pode não ter referências diretas a um objeto que ele domina. Ou seja, a árvore do doador não é uma árvore spanning do gráfico.

No diagrama a seguir:

  • O nó 1 domina o nó 2
  • O nó 2 domina os nós 3, 4 e 6
  • O nó 3 domina o nó 5
  • O nó 5 domina o nó 8
  • O nó 6 domina o nó 7

Estrutura de árvore dominante.

No exemplo abaixo, o nó #3 é o dominante de #10, mas #7 também existe em todos os caminhos simples do GC para #10. Portanto, um objeto B é um dominante de um objeto A se B existir em todos os caminhos simples da raiz até o objeto A.

Ilustração de um dominator animado.

Detalhes do V8

Ao analisar a memória, é útil entender por que os snapshots de heap têm uma determinada aparência. Esta seção descreve alguns tópicos relacionados à memória que correspondem especificamente à máquina virtual V8 JavaScript (VM do V8 ou VM).

Representação de objeto JavaScript

Há três tipos primitivos:

  • Números (por exemplo, 3,14159..)
  • Booleanos (verdadeiro ou falso)
  • Strings (por exemplo, 'Werner Heisenberg')

Eles não podem referenciar outros valores e são sempre folhas ou nós terminais.

Os números podem ser armazenados como:

  • valores inteiros imediatos de 31 bits chamados de números inteiros pequenos (SMIs); ou
  • objetos de heap, chamados de números de heap. Os números de pilha são usados para armazenar valores que não se encaixam no formulário SMI, como doubles, ou quando um valor precisa ser encaminhado, como definir propriedades nele.

As strings podem ser armazenadas em:

  • o heap da VM ou
  • externamente na memória do renderizador. Um objeto wrapper é criado e usado para acessar o armazenamento externo, onde, por exemplo, as origens de script e outros conteúdos recebidos da Web são armazenados, em vez de copiados para a pilha da VM.

A memória para novos objetos JavaScript é alocada de um heap JavaScript dedicado (ou heap da VM). Esses objetos são gerenciados pelo coletor de lixo do V8 e, portanto, permanecem ativos enquanto houver pelo menos uma referência forte a eles.

Objetos nativos são todos os outros objetos que não estão no heap do JavaScript. O objeto nativo, em contraste com o objeto de heap, não é gerenciado pelo coletor de lixo V8 durante o ciclo de vida e só pode ser acessado pelo JavaScript usando o objeto wrapper do JavaScript.

A string Cons é um objeto que consiste em pares de strings armazenadas e unidas, e é um resultado de concatenação. A união do conteúdo da cons string ocorre apenas quando necessário. Um exemplo seria quando uma substring de uma string combinada precisa ser construída.

Por exemplo, se você concatenar a e b, vai receber uma string (a, b) que representa o resultado da concatenação. Se você concatenar d com esse resultado, vai receber outra string de cons ((a, b), d).

Matrizes: uma matriz é um objeto com chaves numéricas. Eles são usados extensivamente na VM V8 para armazenar grandes quantidades de dados. Os conjuntos de pares de chave-valor usados como dicionários são armazenados em matrizes.

Um objeto JavaScript típico pode ser um dos dois tipos de matriz usados para armazenamento:

  • propriedades nomeadas e
  • elementos numéricos

Nos casos em que há um número muito pequeno de propriedades, elas podem ser armazenadas internamente no próprio objeto JavaScript.

Mapa: um objeto que descreve o tipo de objeto e o layout dele. Por exemplo, os mapas são usados para descrever hierarquias de objetos implícitas para acesso rápido a propriedades.

Grupos de objetos

Cada grupo de objetos nativos é composto por objetos que mantêm referências mútuas. Considere, por exemplo, um subárvore DOM em que cada nó tem um link para o pai e links para o próximo filho e o próximo irmão, formando um gráfico conectado. Os objetos nativos não são representados na pilha do JavaScript. É por isso que eles têm tamanho zero. Em vez disso, objetos de wrapper são criados.

Cada objeto wrapper contém uma referência ao objeto nativo correspondente para redirecionar comandos para ele. Por sua vez, um grupo de objetos contém objetos de wrapper. No entanto, isso não cria um ciclo não coletável, porque o GC é inteligente o suficiente para liberar grupos de objetos cujos wrappers não são mais referenciados. No entanto, se você esquecer de liberar um único wrapper, o grupo inteiro e os wrappers associados serão mantidos.