Terminologia de memória

Meggin Kearney
Meggin Kearney

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

Os termos e conceitos descritos aqui se referem ao Criador de perfil de pilha do Chrome DevTools. Se você já trabalhou com Java, .NET ou algum outro Memory Profiler, talvez isso seja uma atualização.

Tamanhos de objetos

Pense na memória como um gráfico com tipos primitivos (como números e strings) e objetos (matrizes associativas). Ele pode ser representado visualmente como um gráfico com vários pontos interconectados da seguinte forma:

Representação visual da memória

Um objeto pode reter memória de duas maneiras:

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

Ao trabalhar com o Heap Profiler no DevTools (uma ferramenta para investigar problemas de memória encontrada em "Perfis"), você provavelmente vai analisar algumas colunas diferentes de informações. Duas que se destacam são Shallow size e RetainedSize, mas o que elas representam?

Tamanho superficial e retido

Tamanho superficial

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

Os objetos JavaScript típicos têm memória reservada para a descrição e o armazenamento de valores imediatos. Normalmente, somente 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 de wrapper no heap JavaScript.

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

Tamanho mantido

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

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

Há muitas raízes GC internas, e a maioria delas não é interessante para os usuários. Do ponto de vista dos aplicativos, há os seguintes tipos de raízes:

  • 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.
  • A árvore do DOM do documento que consiste em todos os nós do DOM nativos acessíveis que passam pelo documento. Nem todos podem ter wrappers JS, mas, se tiverem, os wrappers permanecerão 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 resumos de pilha 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 Node.js. Você não controla a coleta de lixo desse objeto raiz.

Não é possível controlar o objeto raiz

Tudo que não é acessível pela raiz recebe coleta de lixo.

Árvore de retenção de objetos

A heap é uma rede de objetos interconectados. No mundo matemático, essa estrutura é chamada de gráfico ou de memória. Um gráfico é construído com base em nós conectados por meio de bordas, que recebem rótulos.

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

Saiba como registrar um perfil usando o Heap Profiler. Alguns dos itens atraentes que podemos ver no registro do Heap Profiler abaixo incluem a distância: a distância da raiz de GC. Se quase todos os objetos do mesmo tipo estiverem à mesma distância e alguns estiverem a uma distância maior, isso é algo que vale a pena investigar.

Distância da raiz

Dominadores

Os objetos dominadores são compostos de uma estrutura de árvore porque cada objeto tem exatamente um dominador. Um dominador de um objeto pode não ter referências diretas a um objeto que domina, ou seja, a árvore do dominador não é uma árvore abrangente do gráfico.

No diagrama abaixo:

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

Estrutura de árvore dos dominadores

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

Ilustração animada de um dominador

Especificações do V8

Ao criar um perfil de memória, é útil entender por que os snapshots de pilha têm uma determinada aparência. Nesta seção, descrevemos alguns tópicos relacionados à memória que correspondem especificamente à máquina virtual JavaScript V8 (VM ou VM V8).

Representação do 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 fazer referência a outros valores e são sempre folhas ou nós de encerramento.

Números podem ser armazenados como:

  • valores inteiros imediatos de 31 bits chamados de números inteiros pequenos (SMIs, na sigla em inglês) ou
  • objetos de heap, chamados de números de heap. Os números de heap são usados para armazenar valores que não se encaixam no formulário de SMI, como duplos, ou quando um valor precisa ser demarcado, como a definição de propriedades.

As strings podem ser armazenadas:

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

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

Os objetos nativos são todo o restante que não está na heap JavaScript. Um objeto nativo, em contraste com o objeto de heap, não é gerenciado pelo coletor de lixo do V8 durante todo o ciclo de vida e só pode ser acessado pelo JavaScript usando o objeto wrapper do JavaScript.

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

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

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

Um objeto JavaScript típico pode ser um de 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.

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

Grupos de objetos

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

Cada objeto wrapper contém uma referência ao objeto nativo correspondente para redirecionar comandos a ele. Por outro lado, um grupo de objetos contém objetos wrapper. No entanto, isso não cria um ciclo não coletável, já que o GC é inteligente o suficiente para liberar grupos de objetos cujos wrappers não são mais referenciados. No entanto, esquecer de lançar um único wrapper vai conter todo o grupo e os wrappers associados.