DEV Community

Cover image for Node.js Por Baixo dos Panos #9: Coletando o Lixo
Lucas Santos
Lucas Santos

Posted on • Updated on

Node.js Por Baixo dos Panos #9: Coletando o Lixo

Photo por Jilbert Ebrahimi no Unsplash

Em nosso artigo mais recente, passamos por bytecodes! Agora vamos ver algo um pouco mais a fundo!

Garbage Collection

Houve um tempo em que os humanos precisavam escrever código pensando no gerenciamento de memória, mas, com o passar dos anos, não precisamos mais nos preocupar com isso. Isso ocorre devido a uma ferramenta mágica chamada Garbage Collector (GC).

A coleta de lixo é uma prática comum para gerenciamento de memória na maioria das linguagens. O único trabalho de um GC é recuperar a memória que está sendo ocupada por objetos não utilizados. Foi usado pela primeira vez no LISP em 1959.

Mas como ele sabe quando um objeto não é mais usado?

Gerenciamento de memória no Node.js

Como não precisamos mais nos preocupar com a memória, ela é totalmente gerenciada pelo compilador. Portanto, a alocação de memória é feita automaticamente quando precisamos alocar uma nova variável, e é limpa automaticamente quando essa memória não é mais necessária.

A maneira como o GC sabe quando os objetos não são mais usados é por suas referências ou como eles referenciam um ao outro. Quando um objeto não está fazendo referência nem sendo referenciado por qualquer outro objeto, é coletado como lixo. Dê uma olhada neste diagrama:

Você pode ver que existem alguns objetos referenciando outros e referenciados, mas existem dois objetos que não estão sendo referenciados ou fazem referência a ninguém. Portanto, eles serão excluídos e sua memória recuperada. Este é o diagrama após a varredura do GC:

As desvantagens do uso de coletores de lixo são que eles podem ter um enorme impacto no desempenho e ter travamentos e freezes imprevisíveis.

Gerenciamento de memória na prática

Vamos usa um exemplo simples para mostrar como o gerenciamento de memória funciona:

function add (a, b) {
  return a + b
}
add(4, 5)
Enter fullscreen mode Exit fullscreen mode

Temos algumas camadas que precisamos entender:

  • A Stack (ou pilha): a pilha é o local onde todas as variáveis locais, ponteiros para objetos ou fluxo de controle do programa estão. Em nossa função, ambos os parâmetros serão colocados na pilha.
  • A Heap: O heap é a parte do nosso programa onde objetos de instanciados são armazenados, como strings ou objetos. Portanto, o objeto Point abaixo será colocado na heap.
function Point (x, y) {
  this.x = x
  this.y = y
}

const point1 = new Point(1, 2)
Enter fullscreen mode Exit fullscreen mode

Se dermos uma olhada na memória na heap, teríamos algo como isto:

root -----------> point1
Enter fullscreen mode Exit fullscreen mode

Agora vamos criar outros Point:

function Point (x, y) {
  this.x = x
  this.y = y
}

const point1 = new Point(1, 2)
const point2 = new Point(2, 3)
const point3 = new Point(4, 4)
Enter fullscreen mode Exit fullscreen mode

Teriamos isso:

     |-------------------> point1
root |-------------------> point2
     |-------------------> point3
Enter fullscreen mode Exit fullscreen mode

Agora, se o GC fosse executado, nada aconteceria, pois todo os nossos objetos armazenam referências ao objeto raiz.

Vamos adicionar alguns objetos no meio:

function Chart (name) {
  this.name = name
}

function Point (x, y, name) {
  this.x = x
  this.y = y
  this.name = new Chart(name)
}

const point1 = new Point(1, 2, 'Chart1')
const point2 = new Point(2, 3, 'Chart2')
const point3 = new Point(4, 4, 'Chart3')
Enter fullscreen mode Exit fullscreen mode

Agora teríamos isso:

     |-------------------> point1 ----> Chart1
root |-------------------> point2 ----> Chart2
     |-------------------> point3 ----> Chart3
Enter fullscreen mode Exit fullscreen mode

O que aconteceria se a gente setasse nosso point2 para undefined?

     |-------------------> point1 ----> Chart1
root |                     point2 ----> Chart2
     |-------------------> point3 ----> Chart3
Enter fullscreen mode Exit fullscreen mode

Observe que, agora, o objeto point2 não pode ser alcançado a partir do objeto raiz. Portanto, na próxima rodada do GC, ele será eliminado:

     |-------------------> point1 ----> Chart1
root
     |-------------------> point3 ----> Chart3
Enter fullscreen mode Exit fullscreen mode

É basicamente assim que o GC funciona, ele caminha da raiz para todos os objetos, se houver houver objetos na lista de objetos que não tenham sido acessados por essa caminhada, então ele não poderá ser acessado pela raiz e, portanto, será removido.

O GC pode rodar de diferentes formas.

Métodos de GC

Existem vários métodos para executar um GC.

New Space & Old Space

Este é o método que o Node.js usa.

A heap possui dois segmentos principais: o new space e o old space. O new space é onde as alocações estão acontecendo ativamente; este é o local mais rápido para coletar lixo, o new space tem cerca de 1 a 8 MBs. Todos os objetos no new space são chamados de geração jovem (young generation).

Por outro lado, o old space é o local onde os objetos que sobreviveram à última coleta de lixo residem, no nosso caso, os objetos point1 e point3 estão no old space. Eles são chamados de geração antiga (old generation). A alocação no old space é bastante rápida, no entanto, o GC é caro, por isso quase nunca é realizado.

Porém, quase 20% da geração jovem sobrevive e é promovida à geração antiga; portanto, essa varredura no old space não precisa ser feita com muita frequência. Ele é executado apenas quando esse espaço está se esgotando, o que significa em torno de 512mb.

Você pode definir esse limite com o sinalizador --max-old-size-size no Node.js.

Para recuperar a memória antiga, o GC usa dois algoritmos de coleta diferentes.

Scavenge & Mark-Sweep Collection

O método Scavenge é rápido, por isso roda somente an geração jovem. Enquanto o Mark-Sweep é mais lento e roda somente no old space.

O Mark & Sweep functiona somente com alguns passos bem básicos:

  1. Começa com o objeto raiz. Raízes são variáveis globais que são referenciadas no código. Em JS, esse pode ser o objeto window ou, no Node, o objeto global. A lista completa de todas essas raízes é criada pelo GC.
  2. O algoritmo inspeciona todas as raízes e todos os seus filhos, marcando cada um como ativo - de modo que significa que ainda não são lixo - logicamente, qualquer outra coisa que a raiz não possa alcançar não será marcada como ativa, o que significa: lixo
  3. Depois disso, todos os objetos não ativos são liberados.

Conclusão

Estamos a um artigo de terminar nossa série! Neste artigo, discutimos o tratamento da memória e a coleta de lixo; no próximo, discutiremos como o compilador otimiza todo o código! Fiquem ligados!

Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!

Top comments (0)