Terminologia de memória

Meggin keney
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 perfis de memória para diferentes linguagens.

Os termos e noções descritos aqui se referem ao Criador de perfil de heap do Chrome DevTools (link em inglês). Se você já trabalhou com Java, .NET ou outro criador de perfil de memória, isso pode ser 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). Isso pode ser representado visualmente como um gráfico com vários pontos interconectados, da seguinte maneira:

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, impedindo que esses objetos sejam descartados automaticamente por um coletor de lixo (abreviada como GC).

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

Tamanho superficial e mantido

Tamanho superficial

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

Objetos JavaScript típicos têm memória reservada para a descrição e 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 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 de 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 armazenar uma grande quantidade de memória indiretamente, evitando que outros objetos sejam descartados pelo processo automático de coleta de lixo.

Tamanho retido

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

As raízes GC são compostas por identificadores criados (local ou global) 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 em detalhes da implementação do navegador pode ser confuso. Você não precisa se preocupar com as raízes de GC e os identificadores.

Existem muitas raízes de GC internas, 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 da 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 consiste em todos os nós do DOM nativos acessíveis que passam pelo documento. Nem todos eles podem ter wrappers JS, mas se tiverem os wrappers, eles estarã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 instantâneos 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 do Node.js. Você não controla como esse objeto raiz é coletado.

Não é possível controlar o objeto raiz

Tudo que for inacessível da raiz vai receber 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 nós conectados por meio de arestas, que recebem rótulos.

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

Saiba como gravar um perfil usando o criador de perfil de heap. Algumas das coisas chamativas que podemos ver na gravação do Heap Profiler abaixo incluem 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 por uma estrutura de árvore porque cada objeto tem exatamente um dominador. Um dominante de um objeto pode não ter referências diretas a um objeto que domina, ou seja, a árvore do dominante não é uma árvore abrangente do gráfico.

No diagrama abaixo:

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

Estrutura de árvore do dominador

No exemplo abaixo, o nó #3 é o dominador de #10, mas #7 também existe em todos os caminhos simples de GC para #10. Portanto, um objeto B será 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 dominante

Especificações do V8

Ao criar perfis de 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, correspondentes especificamente à máquina virtual V8 JavaScript (VM V8 ou VM).

Representação de objetos 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.

Os números podem ser armazenados como:

  • valores inteiros de 31 bits imediatos chamados 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 SMI, como double, ou quando um valor precisa ser box, como para definir propriedades nele.

As strings podem ser armazenadas em:

  • no 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, 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 da 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.

Objetos nativos são todo o restante que não está no heap JavaScript. O objeto nativo, ao contrário do 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 JavaScript.

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

Por exemplo, se você concatenar a e b, obterá uma string (a, b) que representa o resultado da concatenação. Se 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 baseados 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 às propriedades.

Grupos de objetos

Cada grupo de objetos nativos é composto por objetos que contêm referências mútuas entre si. Considere, por exemplo, uma subárvore do DOM em que cada nó tem um link ao seu pai e se vincula ao próximo filho e ao próximo irmão, formando 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 mantém uma referência ao objeto nativo correspondente para redirecionar comandos a ele. Por sua vez, um grupo de objetos contém objetos wrapper. No entanto, isso não cria um ciclo não coletável, já que a GC é inteligente o suficiente para liberar grupos de objetos em que wrappers não são mais referenciados. No entanto, esquecer de liberar um único wrapper vai reter todo o grupo e os wrappers associados.