Skip to content

Instantly share code, notes, and snippets.

@igorlima
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save igorlima/5e2d6de5ccd43291f5fc to your computer and use it in GitHub Desktop.
Save igorlima/5e2d6de5ccd43291f5fc to your computer and use it in GitHub Desktop.

Em nossa última dica semanal sobre desempenho, nós discutimos em detalhes como o loop de evento do Node.js funciona como o orquestrador de requisições, eventos e callbacks. Também solucionamos um caso de loop bloqueante, o que poderia causar estragos no desempenho da aplicação. No post desta semana vamos mergulhar nos fundamentos do coletor de lixo (GC) da V8 e como ele mantém as "chaves para o reino" da otimização em aplicações Node. Também vamos ver algumas ferramentas para classificar problemas de GC e de gerenciamento de memória na V8.

Quando que tudo começou

where did it all start

Contrário à lenda, a V8 não foi desenhada explicitamente para o Node; e sim foi construída para alimentar um navegador rápido (Chrome / Chromium) ou o JavaScript no cliente side (V8.NET). Eventualmente, ele foi colocado no servidor como um motor de alto desempenho para executar JavaScript e se tornou a base do Node. Mas, como qualquer servidor runtime assim como a JVM ou CLR, a V8 é executada em memória e precisa de gerenciamento de memória.

Em alguns sistemas ou linguagem, cabe ao programa aplicativo gerenciar toda a contabilidade de alocação de memória do heap e liberar a memória quando não for mais necessária. Isto é conhecido como gerenciamento de memória manual. Gerenciamento de memória manual pode ser adequado para pequenos programas, mas geralmente não é bem escalável, nem incentiva a programação modular ou orientada a objeto.

O que/quem é o Coletor de Lixo?

who or what is the Garbage Collector

A V8 engloba o coletor de lixo (GC), também conhecido como memória gerenciada. O GC oferece enorme simplificação para os desenvolvedores por não ter que lidar explicitamente com a contabilidade de memória no código como era feito na época do "C". Ele reduz uma grande quantidade de erros e falhas de memória, que são típicas em grandes aplicações de longa execução e, em alguns casos, pode até mesmo melhorar o desempenho.

No entanto, você também entrega o controle sobre o gerenciamento de memória, que pode ser um problema especialmente para aplicação móvel, que visa a otimização dos recursos do dispositivo. Em especial para o JavaScript, a especificação ECMAScript não expõe qualquer interface para o coletor de lixo bloqueando visibilidade e um GC forçado.

Em termos de desempenho, é um banho de limpeza. Em C, alocar (malloc) e liberar objetos pode ser caro, uma vez que a contabilidade da heap tende a ser mais complicada. Com a memória gerenciada, a alocação geralmente significa apenas incrementar um ponteiro, mas você eventualmente pode pagar quando esgotar a memória e o coletor de lixo entrar em ação. O fato é que a V8 utiliza a coleta de lixo pro bem ou pro mal.

Como a V8 organiza a heap?

how does V8 organize the heap

A V8 divide a heap em vários espaços diferentes para um gerenciamento eficaz de memória:

  • New-space: A maioria dos objetos são alocados nesse espaço. New-space é pequeno e é projetado para conter rapidamente o lixo coletado.
  • Old-pointer-space: Contém a maioria dos objetos que podem ter ponteiros para outros objetos. A maioria dos objetos são movidos para este espaço depois de sobreviver por um tempo no espaço new-space.
  • Old-data-space: Contém objetos que contêm apenas os dados brutos (sem ponteiros para outros objetos). Strings, números encaixotados, e matrizes de duplas desembaladas são movidos para este espaço depois de sobreviver por um tempo no new-space.
  • Large-object-space: Contém objetos que são maiores do que os tamanho limites de outros espaços. Cada objeto tem sua própria região mmap’d de memória. Objetos grandes nunca são movidos pelo coletor de lixo.
  • Code-space: Objetos de código, que contêm instruções JITed, são alocados neste espaço. Este é o único espaço com memória executável.
  • Cell-space, property-cell-space e map-space: Contêm respectivamente Cells, PropertyCells e Maps. Cada espaço contém objetos que são todos do mesmo tamanho e é restrito de ponteiros, o que simplifica a coleção.

Cada espaço é composto por um conjunto de “páginas”. Uma Página é um pedaço contíguo de memória, alocado a partir do sistema operacional com mmap. As páginas sempre são 1 MB de tamanho e de 1 MB alinhamento, exceto em espaço de grande objeto, onde podem ser maiores.

Como é que o coletor de lixo funciona?

how does the garbage collector work

O conceito central de gerenciamento de memória de uma aplicação Node.js / JavaScript é um conceito de acessibilidade.

  1. Um conjunto distinto de objetos é considerado por estar acessível ou em um escopo vivo: estes são conhecidos como "raízes". Normalmente, estes incluem todos os objetos referenciados a partir de qualquer lugar na pilha de chamadas/call stack (ou seja, todos os parâmetros e as variáveis locais nas funções que atualmente estão sendo chamadas) e todas as variáveis globais.
  2. Objetos são mantidos na memória, enquanto eles são acessíveis a partir de raízes através de uma referência ou uma cadeia de referências.
  3. Objetos raiz são apontados diretamente da V8 ou do navegador Web como elementos DOM.

A resolução de problemas fundamental do coletor de lixo é identificar regiões de memória mortos (objetos inalcançáveis / lixo) que não são acessíveis através de alguma cadeia de ponteiros de um objeto que está vivo. Uma vez identificadas, essas regiões podem ser reutilizadas para novas alocações ou liberadas de volta para o sistema operacional.

Para garantir alocação rápida de objeto, há coleta de lixo com pausas curtas, e os ”sem fragmentação de memória V8” empregam as paradas geracionais e exatas do coletor de lixo. A V8 essencialmente:

  • interrompe a execução do programa ao executar um ciclo de coleta de lixo.
  • processa apenas uma parte do objeto heap na maioria dos ciclos de coleta de lixo. Isso minimiza o impacto de parar a aplicação.
  • sempre sabe exatamente onde todos os objetos e ponteiros estão na memória. Isso evita falsamente identificar objetos como ponteiros que pode resultar em vazamentos de memória.

Na V8, o objeto heap é segmentado em várias partes; Portanto, se um objeto é movido em um ciclo da coleta de lixo, a V8 atualiza todos os ponteiros para o objeto.

Não, não... como é que isso realmente funciona?

O GC precisa seguir os ponteiros, a fim de descobrir objetos vivos. A maioria dos algoritmos do coletor de lixo pode migrar objetos de uma parte da memória para uma outra (para reduzir a fragmentação e aumentar a localidade), por isso precisamos também ser capaz de reescrever ponteiros sem atrapalhar os dados simples antigos.

A V8 usa ponteiros "etiquetados". A maioria dos objetos na heap só contêm uma lista de palavras etiquetadas, para que o coletor de lixo possa digitalizar rapidamente, seguindo os ponteiros e ignorando os inteiros. Alguns objetos, como as strings, são conhecidos por conter apenas os dados (sem ponteiros), para que seu conteúdo não seja etiquetado.

Em seguida, vamos verificar quais algoritmos a V8 utiliza para executar o coletor de lixo.

Limpeza / Coleta de lixo curta

A V8 divide a heap em duas gerações. Os objetos são alocados no espaço new-space, que é relativamente pequeno (entre 1 e 8 MB, dependendo da heurística de comportamento). A alocação no espaço new-space é muito barato: só temos um ponteiro de alocação que podemos incrementar sempre que queremos reservar espaço para um novo objeto. Quando o ponteiro de alocação atinge o final do espaço new-space, uma limpeza (o menor ciclo da coleta de lixo) é acionada, o que remove rapidamente os objetos mortos do new-space.

short GC/scavenging

Scavenging/copying (limpeza/cópia) é um tipo de rastreamento do coletor de lixo, que opera realocando objetos acessíveis e, em seguida, recupera os objetos que são deixados para trás, os quais devem ser inacessível e, portanto, mortos.

Uma cópia de coleção de lixo se baseia em ser capaz de encontrar e corrigir todas as referências a objetos copiados.

O algoritmo de limpeza é ótimo para rapidamente coletar e compactar uma pequena quantidade de memória, mas exige uma sobrecarga grande de espaço, desde que nós precisamos de memória física de apoio tanto para o espaço e a partir do espaço. Isto é aceitável enquanto mantemos o espaço new-space pequeno, mas é impraticável usar essa abordagem para alguns megabytes a mais.

A limpeza deve ser rápido por design. Por isso, é adequada a frequente ocorrência de ciclos curtos do GC.

Sinal de varredura & compactação / Coleta de lixo completa

Objetos que tenham sobrevividos a duas coletas de lixo menores são promovidos ao espaço “old-space”. Old-space é o lixo coletado pelo GC completo (o maior ciclo da coleta de lixo), que é muito menos freqüente. Um ciclo completo do GC é acionado quando chegamos sobre um determinado espaço de memória no old-space.

Para a coleta do old-space, que pode conter várias centenas de megabytes de dados, usamos dois algoritmos estreitamente relacionados, Mark-sweep and Mark-compact (sinal de varredura e sinal de compactação.

algorithms for mark-sweep and mark-compact

A coleção de sinal de varredura é uma espécie de rastreamento de coleta de lixo que funciona através da marcação de objetos acessíveis, que em seguida varre a memória e recicla os objetos que não estão marcados (os quais devem ser inacessíveis), colocando-os em uma lista livre.

A fase de sinalização segue cadeias de referência para marcar todos os objetos acessíveis. Uma vez que a sinalização está completa, podemos recuperar a memória através de uma ou outra varredura ou compactação. Ambos os algoritmos trabalham em um nível de página. A fase de varredura executa um passe seqüencial (ordem de endereço) sobre a memória para reciclar todos os objetos não sinalizados. Uma coleção de sinal de varredura não move os objetos.

A coleção de sinal de compactação é um tipo de rastreamento de coleta de lixo que funciona em marcar objetos acessíveis e, em seguida, compactar os objetos marcados (que devem incluir todos os objetos vivos).

A fase de marcação segue cadeias de referência para marcar todos os objetos acessíveis; a fase de compactação normalmente executa um número de passos seqüenciais na memória para mover objetos e atualizar as referências. Devido à compactação, todos os objetos marcados são movidos em um único bloco contíguo de memória (ou um pequeno número de tais blocos); a memória não utilizada após a compactação é reciclada. O algoritmo de compactação também tenta reduzir o uso da memória real através da migração de objetos de páginas fragmentadas (contendo um monte de pequenos espaços livres) para espaços livres em outras páginas.

No entanto, o tempo de pausa no sinal de varredura e compactação foram considerados elevados, diminuindo o desempenho geral.

Sinalização incremental e varredura preguiçosa

Em meados de 2012, o Google introduziu duas melhorias que reduziram as pausas da coleta de lixo significativamente: a marcação incremental e a varredura preguiçosa.

incremental marking and lazy sweeping

Marcação incremental significa a capacidade de fazer um pouco de trabalho de marcação, assim o modificador (programa JavaScript) executa um pouco, e depois volta a fazer mais um pouco o trabalho de marcação. Assim, em vez de uma longa pausa para marcação, o GC marca a heap em uma série de pausas curtas na ordem de 5-10 ms cada por exemplo. A marcação incremental começa quando a heap atinge um determinado tamanho de limiar. Após esse limite, todas as vezes uma certa quantidade de memória é alocada, e a execução é pausada para executar uma etapa de marcação incremental.

Se a taxa de alocação for elevada durante o GC incremental, o motor pode ficar sem memória antes de terminar o ciclo incremental. Nesse caso, o motor deve reiniciar imediatamente um GC completo, não incremental, a fim de recuperar parte da memória e continuar a execução.

Após a marcação, a varredura preguiçosa começa. Todos os objetos foram marcados como vivo ou morto, e o heap sabe exatamente o quanto de memória pode ser liberada pela varredura. Toda esta memória não precisa necessariamente ser liberada imediatamente, e o atraso da varredura porém não dói. Portanto, ao invés de fazer de uma vez só, o coletor de lixo varre as páginas em uma base conforme necessário até que todas as páginas têm sido varridas. Nesse ponto, o ciclo de coleta de lixo está completo, e a marcação incremental é livre para começar de novo.

Monitoramento da Coleta de Lixo com StrongOps

O gráfico Heap Size do StrongOps monitora, ao longo do tempo, três métricas chave de desempenho da memória:

  • Heap: tamanho atual da heap (MB)
  • RSS: tamanho da Resident Set, que é a parte da memória do processo mantida na RAM (MB)
  • V8 Full GC: amostragem do tamanho da heap imediatamente após a coleta de lixo completa (MB)

O que é StrongOps? É um serviço para monitoramento de desempenho e um painel para gerenciamento de cluster para aplicações Node. Clique aqui para conhecer.

StrongOps

Essas métricas sobre escalas históricas temporais (1/3/6/12/24 horas) ajudam a detectar padrões numa coleta efetiva de lixo. Um sistema bem ajustado não vai esperar muito tempo para a execução dos ciclos completos da GC, nem muitos eventos de coleta de lixo curta. Cada evento GC essencialmente causa uma requisição de processamento.

Heap Profiler

A ferramenta Heap profiler fornece informações detalhadas para falhas no tamanho da alocação da heap e a contagem de instâncias de cada objeto ao longo dos ciclos iterativos da coleta de lixo. Deve-se observar os objetos cuja contagem não é relativamente reduzida após cada ciclo de GC. Esses objetos podem ser vazamento de memória.

Novas funcionalidades na Coleta de Lixo

O co-fundador da StrongLoop, Ben Noordhuis, tem também trabalhado para expor métricas adicionais da coleta de lixo como atividade de GC por-geração/por-espaço. Nota: O GC da V8 é geracional com espaços separados para código, dados e objetos grandes. Portanto, tal visibilidade será a chave para ser capaz de solucionar problemas reais do GC. Ben também está trabalhando na criação de rastreamento de pilha Heap Allocation no Node. Então, mantenha-se sintonizado através do StrongLoop Blog ou do nosso Twitter para atualizações sobre este assunto.

E o que mais?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment