A maioria dos desenvolvedores raramente pensa sobre como o gerenciamento de memória JavaScript é implementado. O mecanismo geralmente faz tudo para o programador, portanto, não faz sentido para o último pensar sobre os princípios do mecanismo de gerenciamento de memória.
Mas, mais cedo ou mais tarde, os desenvolvedores ainda terão que lidar com problemas de memória, como vazamentos. Bem, só será possível lidar com eles quando houver um entendimento do mecanismo de alocação de memória. Este artigo é dedicado a explicações. Ele também fornece dicas sobre os tipos mais comuns de vazamentos de memória em JavaScript.
Ciclo de vida da memória
Ao criar funções, variáveis, etc. em JavaScript, o mecanismo aloca uma certa quantidade de memória. Então ele o libera - depois que a memória não é mais necessária.
Na verdade, a alocação de memória pode ser chamada de processo de reservar uma certa quantidade de memória. Pois bem, sua liberação é a devolução da reserva ao sistema. Você pode reutilizá-lo quantas vezes quiser.
Quando uma variável é declarada ou uma função é criada, a memória passa pelo próximo loop.
Aqui em blocos:
- Alocar é a alocação de memória que o mecanismo faz. Ele aloca a memória necessária para o objeto criado.
- Uso - uso de memória. O desenvolvedor é o responsável por este momento, escrever o código para ler e escrever na memória.
- Release - liberando memória. É aqui que o JavaScript entra em ação novamente. Depois que a reserva for liberada, a memória também pode ser usada para outros fins.
“Objetos” no contexto de gerenciamento de memória significa não apenas objetos JS, mas também funções e escopos.
Pilha e heap de memória
Em geral, tudo parece claro - o JavaScript aloca memória para tudo o que o desenvolvedor especifica no código e, então, quando todas as operações são concluídas, a memória é liberada. Mas onde os dados são armazenados?
Existem duas opções - na pilha de memória e na pilha. Qual é a primeira coisa, qual é a segunda - o nome das estruturas de dados que são usadas pelo mecanismo para diferentes propósitos.
A pilha é uma alocação de memória estática
A definição da pilha é conhecida por muitos. É uma estrutura de dados que serve para armazenar dados estáticos, seu tamanho é sempre conhecido em tempo de compilação. JS incluiu valores primitivos como string, número, booleano, indefinido e nulo, bem como referências de função e objeto.
O mecanismo "entende" que o tamanho dos dados não muda, então aloca uma quantidade fixa de memória para cada um dos valores. O processo de alocação de memória antes da execução é chamado de alocação de memória estática.
Como o navegador aloca memória com antecedência para cada tipo de dados, há um limite no tamanho dos dados que podem ser colocados lá. Como o navegador aloca memória com antecedência para cada tipo de dados, há um limite no tamanho dos dados que podem ser colocados lá.
O heap - alocação de memória dinâmica
Quanto ao heap, é tão familiar para muitos quanto a pilha. Ele é usado para armazenar objetos e funções.
Mas, ao contrário da pilha, o mecanismo não pode "saber" quanta memória é necessária para um objeto específico, portanto, a memória é alocada conforme necessário. E essa forma de alocar memória é chamada de "dinâmica" (alocação dinâmica de memória).
Alguns exemplos
Os comentários ao código indicam as nuances da alocação de memória.
const person = {
name: 'John',
age: 24,
};
// JavaScript aloca memória para este objeto no heap.
// Os próprios valores são primitivos, portanto, são armazenados na pilha.
const hobbies = ['hiking', 'reading'];
// Arrays também são objetos, então eles vão para o heap.
deixe o nome = 'John'; // alocar memória para a string
const age = 24; // alocar memória para o número
name = 'John Doe'; // alocar memória para uma nova linha
const firstName = name.slice (0,4); // alocar memória para uma nova linha
// Os valores primitivos são inerentemente imutáveis: em vez de alterar o valor inicial,
// JavaScript cria outro.
Links de JavaScript
Quanto à pilha, todas as variáveis apontam para ela. Se o valor não for primitivo, a pilha conterá uma referência ao objeto heap.
Não há uma ordem específica nele, o que significa que uma referência à área de memória desejada é armazenada na pilha. Em tal situação, o objeto no heap parece um edifício, mas o link é seu endereço.
JS armazena objetos e funções no heap. Mas os valores primitivos e as referências estão na pilha.
Esta imagem mostra a organização de armazenamento de diferentes valores. Observe que a pessoa e a nova pessoa apontam para o mesmo objeto aqui.
Exemplo
const person = {
name: 'John',
age: 24,
};
// Um novo objeto é criado na pilha e uma referência a ele na pilha.
Em geral, os links são extremamente importantes em JavaScript.
Coleta de lixo
Agora é a hora de retornar ao ciclo de vida da memória, ou seja, sua liberação.
O mecanismo JavaScript é responsável não apenas pela alocação de memória, mas também pela desalocação. Nesse caso, o coletor de lixo retorna a memória ao sistema.
E assim que o motor "vê" que a variável ou função não é mais necessária, a memória é liberada.
Mas existe um problema chave aqui. O fato é que é impossível decidir se uma determinada área da memória é necessária ou não. Não existem algoritmos tão precisos que liberem memória em tempo real.
É verdade que existem apenas algoritmos que funcionam bem e permitem que você faça isso. Eles não são perfeitos, mas ainda são muito melhores do que muitos outros. Abaixo - uma história sobre coleta de lixo, que é baseada na contagem de referência, bem como sobre o "algoritmo de sinalização".
E quanto aos links?
Este é um algoritmo muito simples. Ele remove os objetos para os quais nenhum outro ponto de referência. Aqui está um exemplo que explica muito bem.
Se você assistiu ao vídeo, provavelmente percebeu que os hobbies são o único objeto na pilha que foi referenciado na pilha.
Ciclos
A desvantagem do algoritmo é que ele não pode levar em consideração referências circulares. Eles ocorrem quando um ou mais objetos se referem uns aos outros, fora do alcance do ponto de vista do código.
let son = {
name: 'John',
};
let dad = {
name: 'Johnson',
}
son.dad = dad;
dad.son = son;
son = null;
dad = null;
Aqui, filho e pai se referem um ao outro. Não há acesso aos objetos por muito tempo, mas o algoritmo não libera a memória alocada para eles.
Precisamente porque o algoritmo conta referências, atribuir null a objetos não faz nada, já que todo objeto ainda tem uma referência.
Algoritmo para anotações
É aqui que outro algoritmo vem em auxílio, chamado de método de marcação e varredura. Este algoritmo não conta referências, mas determina se diferentes objetos podem ser acessados por meio do objeto raiz. No navegador, esta é a janela, e no Node.js, é global.
Se o objeto não estiver disponível, o algoritmo o marca e depois o remove. Nesse caso, os objetos raiz nunca são destruídos. O problema das referências cíclicas não é relevante aqui - o algoritmo nos permite entender que nem pai nem filho já estão inacessíveis, portanto podem ser "varridos" e a memória devolvida ao sistema.
Desde 2012, absolutamente todos os navegadores são equipados com coletores de lixo que funcionam exatamente de acordo com o método de marcação e varredura.
Não sem suas desvantagens aqui.
Alguém poderia pensar que está tudo bem, e agora você pode esquecer o gerenciamento de memória, deixando tudo para o algoritmo. Mas não é assim.
Grande uso de memória
Como os algoritmos não sabem quando a memória não é mais necessária, os aplicativos JavaScript podem usar mais memória do que precisam. E apenas o coletor pode decidir se deseja ou não liberar a memória alocada.
JavaScript é melhor no gerenciamento de memória em linguagens de baixo nível. Mas também há desvantagens aqui, que devemos ter em mente. Em particular, JS não fornece ferramentas de gerenciamento de memória, ao contrário das linguagens de baixo nível, nas quais o programador lida "manualmente" com a alocação e liberação de memória.
Desempenho
A memória não é apagada a cada novo momento. A liberação é realizada em intervalos regulares. Mas os desenvolvedores não podem saber exatamente quando esses processos são iniciados.
Portanto, em alguns casos, a coleta de lixo pode ter um impacto negativo no desempenho, porque o algoritmo precisa de certos recursos para funcionar. É verdade que a situação raramente se torna completamente incontrolável. Na maioria das vezes, as consequências disso são microscópicas.
Perda de memória
Vazamentos de memória são uma das coisas mais frustrantes no desenvolvimento. Mas se você conhece todos os tipos mais comuns de vazamentos, pode contornar o problema sem muita dificuldade.
Os
vazamentos de memória de variáveis globais ocorrem com mais frequência devido ao armazenamento de dados em variáveis globais.
No navegador, se você cometer um erro e usar var em vez de const ou let, o mecanismo irá anexar a variável ao objeto de janela. Da mesma forma, ele executará a operação nas funções definidas pela palavra função.
user = getUser();
var secondUser = getUser();
function getUser() {
return 'user';
}
// Todas as três variáveis - user, secondUser e
// getUser - serão anexadas ao objeto de janela.
Isso só pode ser feito com funções e variáveis que são declaradas no escopo global. Você pode contornar esse problema executando seu código no modo estrito.
As variáveis globais são frequentemente declaradas intencionalmente; isso nem sempre é um erro. MAS, em qualquer caso, não devemos nos esquecer de liberar memória depois que os dados não forem mais necessários. Para fazer isso, você precisa atribuir nulo à variável global.
window.users = null;
Callbacks e timers
O aplicativo usa mais memória do que deveria, mesmo que nos esqueçamos dos temporizadores e retornos de chamada. O principal problema são os aplicativos de página única (SPA), bem como a adição dinâmica de retornos de chamada e manipuladores de eventos.
Cronômetros esquecidos
const object = {};
const intervalId = setInterval(function() {
// , ,
//
doSomething(object);
}, 2000);
Esta função é executada a cada dois segundos. Sua implementação não deve ser interminável. O problema é que os objetos que têm uma referência no intervalo não são destruídos até que o intervalo seja apagado. Portanto, você precisa prescrever em tempo hábil:
clearInterval (intervalId);
Retorno de chamada esquecido Um
problema pode surgir se um manipulador onClick for anexado a um botão e o próprio botão for removido depois - por exemplo, ele não é mais necessário.
Anteriormente, a maioria dos navegadores simplesmente não conseguia liberar a memória alocada para esse manipulador de eventos. Agora, esse problema é coisa do passado, mas ainda assim, deixar os manipuladores que você não precisa mais não vale a pena.
const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
Elementos DOM esquecidos em variáveis
Isso é semelhante ao caso anterior. O erro ocorre quando os elementos DOM são armazenados em uma variável.
const elements = [];
const element = document.getElementById('button');
elements.push(element);
function removeAllElements() {
elements.forEach((item) => {
document.body.removeChild(document.getElementById(item.id))
});
}
Ao remover qualquer um dos elementos acima, você também deve tomar cuidado ao removê-lo da matriz. Caso contrário, o coletor de lixo não o removerá automaticamente.
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);
});
}
Ao remover um elemento da matriz, você atualiza seu conteúdo com a lista de elementos da página.
Como cada elemento da casa tem uma referência a seu pai, isso evita referencialmente que o coletor de lixo libere a memória ocupada pelo pai, o que leva a vazamentos.
No resíduo seco
O artigo descreve a mecânica geral de alocação de memória, assim como o autor mostrou quais problemas podem surgir e como lidar com eles. Tudo isso é importante para qualquer desenvolvedor Java Script.
, Frontend- Skillbox:
, - ( SPA — Single Page Applications).
, “ ” — ( , ), , , .
— . .
, ( , , ). , , “” — garbage collector.
- (js , garbage collector’a). , .