Todos nós sabemos a importância da coleta de lixo (GC) para o desenvolvimento de aplicativos modernos. Dependendo da sua linguagem de programação, você pode fazer isso por conta própria, como em C. Em outras linguagens, está tão escondido que muitos desenvolvedores mal sabem como é feito.

Em qualquer medida, coleta de lixo é sempre para liberar memória que não está mais sendo usada. As estratégias e algoritmos para fazer isso variam de uma linguagem para outra. JavaScript, por exemplo, segue alguns caminhos interessantes, dependendo se você está em um navegador ou em um servidor Node.js.

Mas você já considerou como esse processo funciona nos bastidores? Vamos dedicar algum tempo para entender como o JavaScript GC faz sua mágica no navegador e no servidor.

Os ciclos de memória

A razão pela qual precisamos do GC é devido às muitas alocações de memória feitas durante a programação. Você cria funções, objetos, etc., e todos eles ocupam espaço.

A grande vantagem do JavaScript quando comparado ao C, por exemplo, é que ele faz a alocação de memória automaticamente para você. Este processo é muito simples e leva apenas três etapas bem definidas:

JavaScript Memory Lifecycle Visual
Ciclo de vida da memória JavaScript.

Certo, mas onde o JavaScript armazena esses dados, exatamente? Existem essencialmente dois destinos para os quais o JavaScript envia os dados: o primeiro é o heap da memória e o segundo é a pilha.

A pilha é outro termo que todo mundo já ouviu. É responsável pelo que chamamos de alocação dinâmica de memória. Em outras palavras, esse espaço é reservado para JavaScript para armazenar recursos como objetos e funções conforme necessário, sem limitações à quantidade de memória que pode usar.

Isso difere um pouco da pilha, que é uma estrutura de dados usada para empilhar literalmente elementos como dados primitivos e referências apontando para objetos reais. A estratégia de alocação de pilha é”mais segura”devido ao fato de que sabe quanta memória foi alocada porque foi corrigida.

É importante entender que essas limitações também variam de fornecedor para fornecedor, portanto, preste atenção a isso quando for para grandes utilizações de memória.

Pegue a seguinte listagem de código como exemplo:

//empilhar e empilhar
tarefa const={ nome:'Lavanderia', descrição:'Ligue para Maria para ir com você...',
}; //pilha
let name='Passear com os cachorros';//1
nome='Caminhada; Alimente os cães';//2
const firstTask=name.slice (0, 5);//3

Cada vez que você cria um novo objeto em JavaScript, um espaço na memória heap é dedicado a ele. Seus valores internos são primitivos, no entanto, o que significa que eles serão empilhados na pilha. O mesmo vale para a referência de tarefa .

Quando se trata de casos especiais, como o uso de valores imutáveis ​​(como os primitivos em JavaScript), a linguagem sempre favorece novas alocações em relação ao uso do slot de memória anterior.

Aqui estão as explicações para os comentários de pontos 1–3 no exemplo de código acima:

  1. Simplesmente criando uma nova variável primitiva com um valor de string
  2. Substituindo seu valor por um novo. Quando isso acontece, o JavaScript aloca um novo local na pilha em vez de substituir o valor pelo atual
  3. Não importa quantas vezes você faça isso, seja por atribuição direta ou pelo resultado de um método, o JavaScript sempre fará o mesmo

Algoritmos de coleta de lixo do JavaScript

Ótimo, agora sabemos como o JavaScript lida com a alocação de memória e para onde as coisas vão quando alocadas. Mas como isso libera as coisas?

O coletor de lixo do JavaScript cuida disso, e o processo é tão simples quanto parece: uma vez que um objeto não é mais usado, o GC libera sua memória.

O que não é tão simples nisso é como o JavaScript sabe quais objetos estão sujeitos a serem coletados. E é aqui que os algoritmos entram em cena.

O GC de contagem de referência

Como o próprio nome sugere, essa estratégia percorre os recursos alocados na memória e procura aqueles que têm zero referências apontando para eles.

Vamos usar o snippet de código anterior como referência para entender melhor:

tarefa

 const={ nome:'Lavanderia', descrição:'Ligue para Maria para ir com você...',
}; tarefa='Passear com os cachorros';

Portanto, inicialmente, o objeto task contém vários atributos internos. Então, vamos supor que outro desenvolvedor decidiu que uma tarefa poderia simplesmente ser representada como uma primitiva em si. Portanto, agora, o primeiro objeto de tarefa não tem mais referências apontando para ele, o que o torna disponível para GC.

Espere, isso não pode ser tão simples… na verdade, parece ingênuo! E é.

No entanto, há um caso especial ao qual você deve estar ciente: dependências circulares. Você provavelmente nunca pensou neles antes, porque JavaScript também sabe como lidar com eles. Mas geralmente, eles acontecem desta forma:

tarefa de função

 (n, d) { //... repórter={...}; cessionário={...}; reporter.assignee=cessionário; cessionário.reporter=repórter;
}; minhaTarefa=tarefa ('Roupa','Chame Maria para ir com você...');

Isso provavelmente não representaria uma tarefa funcional em um aplicativo do mundo real, mas é o suficiente para imaginar uma situação em que os atributos internos de dois objetos referenciam um ao outro.

Isso cria um ciclo. Assim que a função for concluída, o GC de contagem de referência do JavaScript não será capaz de interpretar que esses dois objetos podem ser coletados porque eles ainda contêm referências um ao outro.

Esse é um cenário comum que pode facilmente levar a vazamentos de memória em real-aplicativos do mundo. Para evitar isso, o JavaScript nos fornece uma segunda estratégia na frente de batalha.

O algoritmo de marcação e varredura

O algoritmo marcar e varrer é famoso por ser usado por muitas linguagens de programação para coleta de lixo. Resumindo, ele usa uma abordagem inteligente para determinar se um determinado objeto pode ser alcançado a partir do objeto raiz.

Em JavaScript, o objeto raiz é o objeto global se você estiver em um aplicativo Node.js. se você estiver no navegador, é a janela .

O algoritmo começa do topo e desce na hierarquia repetidamente marcando cada um dos objetos que podem ser alcançados (ou seja, que ainda estão sendo referenciados) a partir da raiz e varrendo aqueles que não podem.

Você pode ver agora como o GC coletará reporter e cessionário do exemplo anterior?

E quanto ao Node.js?

O Node (assim como o Chrome) é com tecnologia V8 , o mecanismo JavaScript de código aberto do Google. As notas importantes ocorrem na memória heap do Node.

Vamos dar uma olhada na representação abaixo:

Node New Space Old Space Comparison
Novo espaço vs. espaço antigo.

O heap do nó é dividido em duas partes principais: o novo espaço e o antigo. Como os nomes sugerem, o primeiro é onde novos objetos (conhecidos como a geração jovem) são alocados, enquanto o segundo é o destino para objetos que sobreviveram por longos períodos (a geração anterior).

Consequentemente, a coleta de lixo de objetos no novo espaço ocorre mais rápido do que no antigo. Em média, até 20 por cento dos objetos da geração jovem sobrevivem toras o suficiente para serem promovidos à geração anterior.

Por causa de todas essas peculiaridades, o V8 faz uso de uma estratégia adicional de GC: o limpador.

O limpador

Como vimos, é mais caro para o Node liberar coisas no antigo espaço. Quando deve fazer isso, o algoritmo de marcação e varredura é executado para atingir o objetivo.

O scavenger GC coleta lixo exclusivamente da geração mais jovem. Sua estratégia consiste em selecionar os objetos sobreviventes e movê-los para uma chamada nova página. Para que essa etapa aconteça, o V8 garante que pelo menos metade da geração jovem permaneça vazia; caso contrário, enfrentaria problemas de falta de memória.

A ideia é rastrear todas as referências da geração jovem sem a necessidade de percorrer toda a geração anterior. Além disso, o limpador também mantém um conjunto de referências do antigo espaço que apontam para objetos no novo espaço.

O processo então move os objetos sobreviventes para a nova página em pedaços, continuamente, até que todo o GC seja concluído. Por fim, ele atualiza os ponteiros dos objetos originais que foram movidos.

Conclusão

Claro, esta foi apenas uma visão geral das estratégias de GC no universo JavaScript. O processo é muito mais complexo e merece mais leitura. Eu recomendo fortemente os famosos docs e V8’s falar sobre o coletor de lixo do Orinoco como recursos complementares.

É essencial ter em mente que, como em muitos outros idiomas, não podemos saber com certeza quando o GC será executado. Desde 2019, cabe ao GC realizar a limpeza de vez em quando, e você não pode acioná-la sozinho.

Fora isso, a maneira como você codifica afeta muito a quantidade de memória que o JavaScript alocará. É por isso que é muito importante conhecer as especificidades da alocação de memória do coletor de lixo e as estratégias de liberação de memória. Existem várias ferramentas de lint e dica de código aberto para ajudá-lo a identificar e analisar esses vazamentos, bem como outras armadilhas em seu código. Vá atrás deles!

A postagem Coleta de lixo JavaScript: navegador vs. servidor apareceu primeiro no LogRocket Blog .

Source link