Bom dia amigos!
Na grande maioria dos casos, como desenvolvedores de JavaScript, não precisamos nos preocupar em trabalhar com memória. O motor faz isso por nós.
Porém, um dia você encontrará um problema chamado "vazamento de memória", que só pode ser resolvido sabendo como a memória é alocada em JavaScript.
Neste artigo, explicarei como funcionam a alocação de memória e a coleta de lixo e como evitar alguns dos problemas comuns associados a vazamentos de memória.
Ciclo de vida da memória
Quando você cria uma variável ou função, o mecanismo JavaScript aloca memória para ela e a libera quando não é mais necessária.
Alocar memória é o processo de reservar um espaço específico na memória, e liberar memória é liberar esse espaço para que possa ser usado para outros fins.
Cada vez que uma variável ou função é criada, a memória passa pelos seguintes estágios:
- Alocação de memória - o mecanismo aloca automaticamente memória para o objeto criado
- Uso de memória - ler e gravar dados na memória nada mais é do que gravar e ler dados de uma variável
- Liberando memória - esta etapa também é executada automaticamente pelo motor. Depois que a memória é liberada, ela pode ser usada para outros fins.
Empilhar e empilhar
A próxima pergunta é o que significa memória? Onde os dados são realmente armazenados?
O mecanismo possui dois desses locais: o heap e a pilha. Heap e Stack são estruturas de dados usadas pelo mecanismo para diferentes propósitos.
Pilha: alocação de memória estática
Todos os dados do exemplo são armazenados na pilha porque são primitivos.
Uma pilha é uma estrutura de dados usada para armazenar dados estáticos. Dados estáticos são dados cujo tamanho é conhecido pelo mecanismo no estágio de compilação do código. Em JavaScript, esses dados são primitivos (strings, números, booleanos, indefinidos e nulos) e referências que apontam para objetos e funções.
Como o mecanismo sabe que o tamanho dos dados não mudará, ele aloca um tamanho fixo de memória para cada valor. O processo de alocar memória antes de executar seu código é chamado de alocação de memória estática. Como o mecanismo aloca um tamanho fixo de memória, existem certos limites para esse tamanho, que são altamente dependentes do navegador.
Heap: alocação de memória dinâmica
O heap é para armazenar objetos e funções. Ao contrário da pilha, o mecanismo não aloca um tamanho fixo de memória para objetos. A memória é alocada conforme necessário. Essa alocação de memória é chamada de dinâmica. Aqui está uma pequena tabela de comparação:
| Pilha | Heap |
|---|---|
| Valores primitivos e referências | Objetos e funções |
| O tamanho é conhecido em tempo de compilação | O tamanho é conhecido em tempo de execução |
| Memória fixa alocada | O tamanho da memória para cada objeto não é limitado |
Exemplos de
Vejamos alguns exemplos.
const person = {
name: "John",
age: 24,
};
O mecanismo aloca memória para este objeto no heap. No entanto, os valores das propriedades são armazenados na pilha.
const hobbies = ["hiking", "reading"];
Arrays são objetos, então eles são armazenados no heap
let name = "John";
const age = 24;
name = "John Doe";
const firstName = name.slice(0, 4);
Os primitivos são imutáveis. Isso significa que em vez de alterar o valor original, o JavaScript cria um novo.
Links
Todas as variáveis são armazenadas na pilha. No caso de valores não primitivos, a pilha armazena referências a um objeto no heap. A memória na pilha está desordenada. É por isso que precisamos de links na pilha. Você pode pensar em links como endereços e objetos como casas em um endereço específico.
Na imagem acima, podemos ver como os diversos valores são armazenados. Observe que pessoa e nova pessoa apontam para o mesmo objeto
Exemplos de
const person = {
name: "John",
age: 24,
};
Isso cria um novo objeto na pilha e uma referência a ele na pilha
Coleta de lixo
Assim que o mecanismo percebe que uma variável ou função não é mais usada, ele libera a memória que ocupa.
Na verdade, o problema de liberar memória não utilizada é insolúvel: não existe um algoritmo perfeito para resolvê-lo.
Neste artigo, examinaremos dois algoritmos que oferecem as melhores soluções até o momento: contagem de referência, coleta de lixo e marcação e varredura.
Coleta de lixo por contagem de referência
Tudo é simples aqui - objetos para os quais nenhum ponto de referência é apagado da memória. Vejamos um exemplo. As linhas representam links.
Observe que apenas o objeto "hobbies" permanece na pilha, uma vez que apenas este objeto é referenciado na pilha.
Links cíclicos
O problema com esse método de coleta de lixo é a incapacidade de definir referências circulares. Esta é uma situação em que dois ou mais objetos apontam um para o outro, mas não têm refexs. Essa. esses objetos não podem ser acessados de fora.
const son = {
name: "John",
};
const dad = {
name: "Johnson",
};
son.dad = dad;
dad.son = son;
son = null;
dad = null;
Como os objetos "filho" e "pai" se referem um ao outro, o algoritmo de contagem de referência não pode liberar memória. No entanto, esses objetos não estão mais disponíveis para código externo.
Algoritmo para marcação e limpeza
Este algoritmo resolve o problema de referências circulares. Em vez de contar as referências que apontam para um objeto, ele determina a acessibilidade do objeto a partir do objeto raiz. O objeto raiz é o objeto "janela" no navegador ou o objeto "global" no Node.js.
O algoritmo marca objetos como inacessíveis e os remove. Assim, as referências circulares não são mais um problema. No exemplo acima, os objetos "pai" e "filho" são inacessíveis a partir do objeto raiz. Eles serão marcados como lixo e removidos. O algoritmo em questão foi implementado em todos os navegadores modernos desde 2012. As melhorias feitas desde então são sobre melhorias de implementação e desempenho, mas não a ideia central do algoritmo.
Compromissos
A coleta de lixo automática nos permite focar na construção de aplicativos e não perder tempo no gerenciamento de memória. No entanto, tudo tem um preço.
Uso de memória
Visto que leva algum tempo para que os algoritmos determinem que a memória não está mais sendo usada, os aplicativos JavaScript tendem a usar mais memória do que realmente precisam.
Mesmo que os objetos estejam marcados como lixo, o coletor deve decidir quando coletá-los para não bloquear o fluxo do programa. Se você deseja que seu aplicativo seja o mais eficiente possível em termos de uso de memória, é melhor usar uma linguagem de programação de nível inferior. Mas tenha em mente que essas linguagens têm suas próprias compensações.
atuação
Os algoritmos de coleta de lixo são executados periodicamente para limpar objetos não utilizados. O problema é que nós, como desenvolvedores, não sabemos exatamente quando isso vai acontecer. Grandes quantidades de coleta de lixo ou coleta de lixo frequente podem afetar o desempenho, pois requer uma certa quantidade de poder de processamento. No entanto, isso geralmente acontece despercebido pelo usuário e desenvolvedor.
Perdas de memória
Vamos dar uma olhada rápida nos problemas de vazamento de memória mais comuns.
Variáveis globais
Se você declarar uma variável sem usar uma das palavras-chave (var, let ou const), a variável se torna uma propriedade do objeto global.
users = getUsers();
Executar seu código no modo estrito evita isso.
Às vezes, declaramos variáveis globais propositalmente. Neste caso, para liberar a memória ocupada por tal variável, você deve atribuir a ela o valor "nulo":
window.users = null;
Timers e callbacks esquecidos
Se você esquecer temporizadores e retornos de chamada, o uso de memória do seu aplicativo pode aumentar drasticamente. Tenha cuidado, especialmente ao criar aplicativos de página única (SPA) onde manipuladores de eventos e retornos de chamada são adicionados dinamicamente.
Cronômetros esquecidos
const object = {};
const intervalId = setInterval(function () {
// , , ,
// ,
doSomething(object);
}, 2000);
O código acima executa a função a cada 2 segundos. Se você não precisar mais do cronômetro, deverá cancelá-lo até:
clearInterval(intervalId);
Isso é especialmente importante para o SPA. Mesmo se você for para outra página onde o cronômetro não está em uso, ele funcionará em segundo plano.
Callbacks esquecidos
Suponha que você registre um manipulador para um clique de botão que posteriormente será excluído. Na verdade, isso não é mais um problema, mas ainda é recomendado remover manipuladores que não são mais necessários:
const element = document.getElementById("button");
const onClick = () => alert("hi");
element.addEventListener("click", onClick);
element.removeEventListener("click", onClick);
element.parentNode.removeChild(element);
Links fora do DOM
Esse vazamento de memória é semelhante aos anteriores, ocorre ao armazenar elementos DOM em JavaScript:
const elements = [];
const element = document.getElementById("button");
elements.push(element);
function removeAllElements() {
elements.forEach((item) => {
document.body.removeChild(document.getElementById(item.id));
});
}
Se você remover qualquer um desses elementos, também deverá removê-lo da matriz. Caso contrário, esses itens não podem ser removidos pelo coletor de lixo:
const elements = [];
const element = document.getElementById("button");
elements.push(element);
function removeAllElements() {
elements.forEach((item, index) => {
document.body.removeChild(document.getElementById(item.id));
elements.splice(index, 1);
});
}
Espero que você tenha encontrado algo interessante para você. Obrigado pela atenção.