4 tipos de vazamentos de memória em JavaScript e como se livrar deles 1/2

b
Saiba mais sobre vazamentos de memória em JavaScript e o que você pode fazer para corrigi-los!
Neste artigo, exploraremos as tipologias de vazamento de memória no código JavaScript do lado do cliente. Também aprenderemos a usar as ferramentas de desenvolvedor do Chrome para encontrá-los. Vamos começar!

Introdução

Vazamentos de memória são um problema que todos os desenvolvedores enfrentam, mesmo trabalhando com linguagens que gerenciam memória, há casos em que ocorrem vazamentos de memória. Vazamentos são a causa de toda uma família de problemas: lentidão, travamentos, tempos de resposta altos e até problemas com outros aplicativos.

O que é um vazamento de memória?

Um vazamento de memória pode ser essencialmente definido como uma área de memória que não é mais usada por um aplicativo, mas que não é liberada e, portanto, não disponibilizada ao sistema operacional. As linguagens têm diferentes métodos de gerenciamento de memória. Esses métodos reduzem as chances de ocorrer um vazamento de memória. No entanto, saber se uma área de memória se tornou inútil ou não é um problema potencialmente insolúvel. Apenas designers são capazes de esclarecer se uma área de memória não é utilizada. A Wikipedia tem bons artigos sobre gerenciamento manual e automático de memória.

Gerenciamento de memória em JavaScript

JavaScript é uma das linguagens com coleta de lixo. As linguagens de coleta de lixo ajudam os desenvolvedores a gerenciar a memória verificando periodicamente qual área de memória alocada anteriormente deve permanecer acessível por outras partes do aplicativo. Em outras palavras, linguagens baseadas em coleta de lixo reduzem o problema de gerenciamento de memória de "qual área de memória ainda é necessária" para "qual área de memória permanece acessível ao restante do aplicativo". A diferença é sutil, mas importante: enquanto apenas o desenvolvedor sabe qual área de memória deve permanecer acessível no futuro, áreas de memória que se tornaram inacessíveis podem ser identificadas por algoritmos e marcadas para serem devolvidas ao SO.

Linguagens livres de lixo normalmente usam outras técnicas para gerenciar memória: gerenciamento explícito de memória, onde os desenvolvedores informam ao compilador quando uma área de memória não é mais necessária, e contagem de referência, na qual uma contagem de uso é associada a cada bloco de memória (quando chegar a zero, a área é devolvida ao SO). Essas técnicas vêm com suas próprias contrapartes (e riscos potenciais de vazamentos).

Vazamentos em JavaScript

O principal motivo de vazamentos nas linguagens de coleta de lixo são referências indesejadas. Para entender o que são referências indesejadas, devemos primeiro entender como o coletor de lixo determina se uma área de memória está acessível ou não.

“A principal razão para vazamentos em linguagens baseadas no Garbage Collector são referências indesejadas”

 

Marcar e varrer

A maioria dos coletores de lixo usa um algoritmo chamado mark-and-sweep. O algoritmo consiste nos seguintes passos:

  1. O coletor de lixo constrói uma lista de “raízes”. As raízes são geralmente variáveis ​​globais que são referenciadas no código. Em JavaScript, o objeto "windows" é um exemplo de variável global que pode atuar como raiz. O objeto Windows está sempre presente, então o Garbage Collector pode considerá-lo - e todos os seus filhos - sempre presentes (ou seja, não Garbage).
  2. Todas as raízes são inspecionadas e marcadas como ativas. Todas as crianças também são inspecionadas recursivamente. Qualquer coisa que possa ser acessada pela raiz não é considerada lixo.
  3. Todas as áreas de memória que não estão marcadas como ativas podem ser consideradas lixo. O Collector pode então liberar essas áreas de memória e devolvê-las ao SO.

A maioria dos coletores de lixo modernos são construídos sobre esse algoritmo com variações, mas o princípio permanece o mesmo: todas as áreas de memória acessíveis são marcadas como tal e o restante é considerado lixo.
Referências indesejadas são referências a áreas de memória que o desenvolvedor sabe que não usa mais, mas que – por uma série de razões – permanecem na árvore de uma raiz ativa. No contexto do JavaScript, referências indesejadas são variáveis ​​que são mantidas em algum lugar do código que não serão mais usadas e apontam para uma área de memória que poderia ter sido liberada. Para alguns, é um erro do desenvolvedor.
Para entender quais são os vazamentos mais comuns em JavaScript, você precisa observar como as referências são frequentemente esquecidas.
a

Os quatro tipos de vazamentos de JavaScript:

1: variáveis ​​globais acidentalmente

Um dos objetivos do JavaScript era desenvolver uma linguagem que se parecesse com Java, mas fosse permissiva o suficiente para ser usada por iniciantes. Isso se reflete na maneira como o JavaScript lida com variáveis ​​não declaradas: uma referência a uma variável não declarada cria uma nova variável no objeto global. No caso de um navegador, o objeto global é window. Em outras palavras:
function foo(arg) {
bar = "this is a hidden global variable";
}
Is in fact:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
Se bar deveria conter a referência a uma variável apenas dentro do escopo da função foo e você esquece de declará-la, uma variável global é criada por acidente. Neste exemplo, vazar uma String não é muito perigoso, mas pode ser muito pior.
Outra maneira de criar acidentalmente uma variável global é:
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

Para evitar que esses erros apareçam, adicione 'use strict'; no início de seus arquivos JavaScript. Isso aciona um modo de análise JavaScript mais estrito que impede os globais por acidente.

Nota sobre variáveis ​​globais
Embora falemos de Globals insuspeitos, a realidade é que muito código está repleto de variáveis ​​globais explícitas. Eles são, por definição, não colecionáveis ​​(a menos que sejam anulados ou reatribuídos). Em particular, as variáveis ​​globais usadas para armazenar e processar grandes quantidades de informações são motivo de preocupação. Se você precisar usar variáveis ​​globais para armazenar muitos dados, certifique-se de anulá-los ou reatribuí-los quando terminar.
Uma causa comum de maior consumo de memória em relação a Globals é o armazenamento em cache. O cache armazena dados que são usados ​​repetidamente. Para que isso seja eficaz, o cache deve ter um limite de tamanho maior. Caches que crescem sem limites resultam em alto consumo de memória porque o conteúdo não pode ser coletado.

2: temporizadores esquecidos ou retornos de chamada

Usar setInterval é bastante comum em JavaScript. As outras bibliotecas fornecem observadores e outros sistemas que suportam retornos de chamada. A maioria dessas bibliotecas torna os retornos de chamada inacessíveis depois que suas próprias instâncias se tornam inacessíveis. No caso de setInterval, é comum ver código assim:
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
Este exemplo ilustra o que pode acontecer com temporizadores pendentes: temporizadores que fazem referência a um nó ou dados que não são mais necessários. O objeto representado pelo nó pode ser excluído no futuro, tornando inútil todo o bloco dentro do manipulador de intervalo.
No entanto, o manipulador, assim como o intervalo, ainda está ativo e, portanto, não pode ser coletado (para isso, o intervalo teria que ser interrompido). Se o intervalo não puder ser coletado, suas dependências também não poderão. Isso significa que os recursos, que armazenam dados potencialmente bastante substanciais, também não podem ser coletados.
No caso de observadores, é importante tornar as chamadas explícitas para removê-las quando não forem mais necessárias (ou os objetos associados estiverem prestes a se tornar inacessíveis). No passado, isso era particularmente importante, pois alguns navegadores (IE6) não conseguiam lidar bem com referências cíclicas (veja abaixo mais informações sobre isso)
Hoje, a maioria dos navegadores pode e irá coletar manipuladores de observadores quando o objeto observado se tornar inacessível. No entanto, é uma boa prática excluir observadores antes que o objeto seja excluído. Por exemplo :
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
Nota sobre observadores de objetos e referências cíclicas
Observadores e referências cíclicas há muito são o pesadelo de um desenvolvedor JavaScript. Isso ocorreu devido a um bug (ou decisão de design) no coletor de lixo do Internet Explorer. Versões mais antigas do IE não podiam detectar referências cíclicas entre nós DOM e código JavaScript. Isso é característico dos observadores, que geralmente mantêm uma referência ao observável (como no exemplo acima). Em outras palavras, toda vez que um observador é adicionado a um nó no Internet Explorer, isso leva a um vazamento de memória. É por isso que os desenvolvedores começaram a remover manualmente os manipuladores antes dos nós ou anular referências nos observadores. Hoje, os navegadores modernos (incluindo o Internet Explorer e o Microsoft Edge) usam algoritmos de coleta de lixo mais modernos que podem detectar esses ciclos e tratá-los corretamente. Em outras palavras, não é mais necessário chamar removeEventListener antes de tornar um nó inacessível.
Frameworks como jQuery removem listeners antes de liberar nós (quando usam sua API específica para isso). Ele é tratado internamente pela biblioteca e garante que não gere vazamentos, mesmo em navegadores mais antigos, como o antigo Internet Explorer.

3: Referências fora do DOM

Às vezes, pode ser útil armazenar nós DOM em estruturas de dados. Suponha que você queira atualizar rapidamente o conteúdo de várias colunas em uma tabela. Pode fazer sentido armazenar uma referência a cada coluna DOM em um dicionário ou array. Quando isso acontece, duas referências ao mesmo elemento DOM são mantidas: uma na árvore DOM e outra no dicionário. Se em algum momento no futuro você decidir excluir essas colunas, deverá tornar ambas as referências inacessíveis.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
Outra coisa a considerar são as referências a nós internos ou folhas da árvore DOM. Suponha, por exemplo, que você mantenha uma referência a uma célula de tabela específica (uma tag) em seu código JavaScript. Em algum momento, você decide remover a tabela específica do DOM, mas mantém a referência a essa célula. Intuitivamente, pode-se pensar que o GC coletará corretamente tudo, menos esta célula. Na prática, isso não acontecerá: esta célula é um nó filho do array e os filhos guardam referências a seus pais. O que significa que toda a tabela permanecerá na memória, devido à referência JavaScript a esta célula. Leve isso em consideração se você mantiver referências a elementos DOM.

4: Fechamento

Um elemento chave do desenvolvimento JavaScript é o fechamento: funções anônimas que capturam variáveis ​​do escopo pai. Os desenvolvedores do Meteor encontraram um caso especial e bastante sutil de vazamento de memória devido aos detalhes da implementação do tempo de execução do Javascript:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
Este trecho faz uma coisa: sempre que substituirCoisa é chamado, a coisa recupera um novo objeto que contém um grande array e um novo fechamento (algum método). Ao mesmo tempo, a variável não utilizada contém uma cerca que tem uma referência a coisa original (a coisa da chamada anterior para substituirCoisa). Já um pouco complicado, certo? O importante é que uma vez que o escopo é criado para as fences, que estão no mesmo escopo pai, este escopo é compartilhado.
Neste caso, o escopo criado para o fechamento algum método é compartilhado com não usado. Não utilizado tem uma referência a coisa original. Embora não usado nunca ser usado. algum método pode ser usado através a coisa. E gosto algum método compartilha o escopo da cerca com não usado, mesmo se não usado nunca é usado, sua referência a coisa original força-o a permanecer ativo (impede sua coleta).
Quando esse snipet é executado repetidamente, vemos um aumento constante no uso de memória. E não reduz com a passagem do GC. Em essência, uma lista encadeada de fechamento é criada (enraizada pela variável a coisa) e cada escopo desses fechamentos contém uma referência ao array grande, o que gera um vazamento consequente.
Este é um artefato de implementação. Outra implementação de cercas que lidaria com esse problema é concebível, conforme explicado no meteoro-blog.
Artigo original de Sebastião Peyrott traduzido por JS Staff
 
Não perca a continuação do próximo episódio: como corrigir vazamentos de memória 🙂
[separator type=”” size=”” icon=”star”] [actionbox color=”default” title=”” description=”JS-REPUBLIC é uma empresa de serviços especializada em desenvolvimento JavaScript. Somos um centro de treinamento aprovado. Encontre todos os nossos treinamentos técnicos em nosso site de parceiros dedicado ao Treinamento” btn_label=”Nosso treinamento” btn_link=”http://training.ux-republic.com” btn_color=”primary” btn_size=”big” btn_icon=”star” btn_external ="1″]