Fundamentos de la gestión de memoria de JavaScript: cómo funciona y qué problemas pueden surgir





La mayoría de los desarrolladores rara vez piensan en cómo se implementa la administración de memoria JavaScript. El motor suele hacer todo por el programador, por lo que no tiene sentido que este último piense en los principios del mecanismo de gestión de la memoria.



Pero tarde o temprano, los desarrolladores todavía tienen que lidiar con problemas de memoria, como fugas. Bueno, será posible tratar con ellos solo cuando se comprenda el mecanismo de asignación de memoria. Este artículo está dedicado a las explicaciones. También proporciona consejos sobre los tipos más comunes de pérdidas de memoria en JavaScript.



Ciclo de vida de la memoria



Al crear funciones, variables, etc. en JavaScript, el motor asigna una cierta cantidad de memoria. Luego lo libera, después de que la memoria ya no es necesaria.



En realidad, la asignación de memoria se puede llamar el proceso de reservar una cierta cantidad de memoria. Bueno, su liberación es el regreso de la reserva al sistema. Puedes reutilizarlo tantas veces como quieras.



Cuando se declara una variable o se crea una función, la memoria pasa por el siguiente ciclo.







Aquí en bloques:



  • Asignar es la asignación de memoria que hace el motor. Asigna la memoria necesaria para el objeto creado.
  • Uso : uso de memoria. El desarrollador es el responsable de este momento, escribiendo en el código para leer y escribir en la memoria.
  • Liberar - liberar memoria. Aquí es donde JavaScript vuelve a cobrar vida. Una vez que se libera la reserva, la memoria también se puede utilizar para otros fines.


“Objetos” en el contexto de la gestión de memoria significa no solo objetos JS, sino también funciones y ámbitos.



Pila y montón de memoria



En general, todo parece estar claro: JavaScript asigna memoria para todo lo que el desarrollador especifica en el código y luego, cuando se completan todas las operaciones, se libera la memoria. Pero, ¿dónde se almacenan los datos?



Hay dos opciones: en la pila de memoria y en el montón. ¿Qué es lo primero, qué es lo segundo? El nombre de las estructuras de datos que utiliza el motor para diferentes propósitos.



La pila es una asignación de memoria estática







La definición de pila es conocida por muchos. Es una estructura de datos que se utiliza para almacenar datos estáticos, su tamaño siempre se conoce en el momento de la compilación. JS ha incluido valores primitivos como cadena, número, booleano, indefinido y nulo, así como referencias a funciones y objetos.



El motor "comprende" que el tamaño de los datos no cambia, por lo que asigna una cantidad fija de memoria para cada uno de los valores. El proceso de asignación de memoria antes de la ejecución se denomina asignación de memoria estática.



Dado que el navegador asigna memoria por adelantado para cada tipo de datos, existe un límite en el tamaño de los datos que se pueden colocar allí. Dado que el navegador asigna memoria por adelantado para cada tipo de datos, existe un límite en el tamaño de los datos que se pueden colocar allí.



El montón : asignación de memoria dinámica



En cuanto al montón, es tan familiar para muchos como la pila. Se utiliza para almacenar objetos y funciones.



Pero a diferencia de la pila, el motor no puede "saber" cuánta memoria se necesita para un objeto en particular, por lo que la memoria se asigna según sea necesario. Y esta forma de asignar memoria se llama "dinámica" (asignación de memoria dinámica).



Algunos ejemplos



Los comentarios al código indican los matices de la asignación de memoria.



const person = {
  name: 'John',
  age: 24,
};
      
      





// JavaScript asigna memoria para este objeto en el montón.

// Los valores en sí mismos son primitivos, por lo que se almacenan en la pila.



const hobbies = ['hiking', 'reading'];
      
      





// Las matrices también son objetos, por lo que van al montón.



let name = 'John'; // asigna memoria para la cadena

const age = 24; // asigna memoria para el número

name = 'John Doe'; // asigna memoria para una nueva línea

const firstName = name.slice (0,4); // asignar memoria para una nueva línea



// Los valores primitivos son inherentemente inmutables: en lugar de cambiar el valor inicial,

// JavaScript crea otro.



Enlaces JavaScript



En cuanto a la pila, todas las variables apuntan a ella. Si el valor no es primitivo, la pila contiene una referencia al objeto del montón.



No hay ningún orden en particular, lo que significa que una referencia al área de memoria deseada se almacena en la pila. En tal situación, el objeto en el montón parece un edificio, pero el enlace es su dirección.



JS almacena objetos y funciones en el montón. Pero los valores primitivos y las referencias están en la pila.







Esta imagen muestra la organización de almacenamiento de diferentes valores. Tenga en cuenta que person y newPerson apuntan al mismo objeto aquí.



Ejemplo



const person = {
  name: 'John',
  age: 24,
};
      
      





// Se crea un nuevo objeto en el montón y una referencia a él en la pila.



En general, los enlaces son extremadamente importantes en JavaScript.



Recolección de basura



Ha llegado el momento de volver al ciclo de vida de la memoria, es decir, a su liberación.



El motor de JavaScript es responsable no solo de la asignación de memoria, sino también de la desasignación. En este caso, el recolector de basura devuelve la memoria al sistema.



Y tan pronto como el motor "ve" que la variable o función ya no es necesaria, se libera la memoria.



Pero aquí hay un problema clave. El hecho es que es imposible decidir si una determinada área de la memoria es necesaria o no. No existen algoritmos tan precisos que liberen memoria en tiempo real.



Es cierto que solo hay algoritmos que funcionan bien y que te permiten hacer esto. No son perfectos, pero son mucho mejores que muchos otros. A continuación: una historia sobre la recolección de basura, que se basa en el recuento de referencias, así como sobre el "algoritmo de marcado".



¿Y los enlaces?



Este es un algoritmo muy simple. Elimina aquellos objetos a los que no hay otros puntos de referencia. Aquí tienes un ejemplo que lo explica bastante bien.



Si ha visto el video, probablemente haya notado que los pasatiempos son el único objeto del montón al que se ha hecho referencia en la pila.



Ciclos



La desventaja del algoritmo es que no puede tener en cuenta referencias circulares. Ocurren cuando uno o más objetos se refieren entre sí, fuera del alcance desde el punto de vista del código.



let son = {
  name: 'John',
};
let dad = {
  name: 'Johnson',
}
 
son.dad = dad;
dad.son = son;
son = null;
dad = null;
      
      







Aquí hijo y papá se refieren el uno al otro. No hay acceso a los objetos durante mucho tiempo, pero el algoritmo no libera la memoria asignada para ellos.



Precisamente porque el algoritmo cuenta referencias, asignar nulos a objetos no hace nada, ya que cada objeto todavía tiene una referencia.



Algoritmo para anotaciones



Aquí es donde otro algoritmo viene al rescate, que se llama método de marca y barrido. Este algoritmo no cuenta las referencias, pero determina si se puede acceder a diferentes objetos a través del objeto raíz. En el navegador, esta es la ventana y en Node.js, es global.







Si el objeto no está disponible, el algoritmo lo marca y luego lo elimina. En este caso, los objetos raíz nunca se destruyen. El problema de las referencias cíclicas no es relevante aquí: el algoritmo nos permite entender que ni el padre ni el hijo ya son inaccesibles, por lo que pueden ser "barridos" y la memoria devuelta al sistema.



Desde 2012, absolutamente todos los navegadores están equipados con recolectores de basura que funcionan exactamente según el método de marcado y barrido.



No sin sus inconvenientes aquí.





Uno pensaría que todo está bien, y ahora puedes olvidarte de la gestión de la memoria, dejando todo al algoritmo. Pero no es así.



Gran uso de memoria



Debido a que los algoritmos no saben cuándo ya no se necesita la memoria, las aplicaciones JavaScript pueden usar más memoria de la que necesitan. Y solo el recolector puede decidir si libera o no la memoria asignada.



JavaScript es mejor para administrar la memoria en lenguajes de bajo nivel. Pero aquí también hay desventajas, que deben tenerse en cuenta. En particular, JS no proporciona herramientas de administración de memoria, a diferencia de los lenguajes de bajo nivel, en los que el programador se ocupa "manualmente" de la asignación y liberación de memoria.



Actuación



La memoria no se borra cada nuevo momento en el tiempo. La liberación se realiza a intervalos regulares. Pero los desarrolladores no pueden saber exactamente cuándo se inician estos procesos.



Por lo tanto, en algunos casos, la recolección de basura puede tener un impacto negativo en el rendimiento, porque el algoritmo necesita ciertos recursos para funcionar. Es cierto que la situación rara vez se vuelve completamente inmanejable. Muy a menudo, las consecuencias de esto son microscópicas.



Pérdidas de memoria



Las pérdidas de memoria son una de las cosas más frustrantes del desarrollo. Pero si conoce todos los tipos de fugas más comunes, podrá solucionar el problema sin mucha dificultad.



Variables globales Las



pérdidas de memoria ocurren con mayor frecuencia debido al almacenamiento de datos en variables globales.



En el navegador, si comete un error y usa var en lugar de const o let, el motor adjuntará la variable al objeto de la ventana. Asimismo, realizará la operación sobre funciones definidas por la palabra función.



user = getUser();
var secondUser = getUser();
function getUser() {
  return 'user';
}
      
      





// Las tres variables (usuario, segundo usuario y

// getUser) se adjuntarán al objeto de la ventana.



Esto solo se puede hacer con funciones y variables declaradas en el ámbito global. Puede solucionar este problema ejecutando su código en modo estricto.



Las variables globales a menudo se declaran intencionalmente; esto no siempre es un error. PERO, en cualquier caso, no debemos olvidarnos de liberar memoria después de que los datos ya no sean necesarios. Para hacer esto, debe asignar un valor nulo a la variable global.



window.users = null;



Devolución de llamada y temporizadores



La aplicación usa más memoria de la que debería, incluso si nos olvidamos de los temporizadores y las devoluciones de llamada. El principal problema son las aplicaciones de una sola página (SPA), así como la adición dinámica de devoluciones de llamada y controladores de eventos.



Temporizadores olvidados



const object = {};
const intervalId = setInterval(function() {
  //     ,   ,
  //     
  doSomething(object);
}, 2000);
      
      





Esta función se ejecuta cada dos segundos. Su implementación no debería ser interminable. El problema es que los objetos que tienen una referencia en el intervalo no se destruyen hasta que se borra el intervalo. Por lo tanto, debe prescribir de manera oportuna:

clearInterval (intervalId);



Devolución de llamada olvidada



Puede surgir un problema si se adjunta un controlador onClick a un botón y el botón en sí se elimina después; por ejemplo, ya no es necesario.



Anteriormente, la mayoría de los navegadores simplemente no podían liberar la memoria asignada para dicho controlador de eventos. Ahora bien, este problema es cosa del pasado, pero aún así, dejar los controladores que ya no necesita no vale la pena.



const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
      
      







Elementos DOM olvidados en variables



Esto es similar al caso anterior. El error ocurre cuando los elementos DOM se almacenan en una variable.



const elements = [];
const element = document.getElementById('button');
elements.push(element);
function removeAllElements() {
  elements.forEach((item) => {
document.body.removeChild(document.getElementById(item.id))
  });
}
      
      





Al eliminar cualquiera de los elementos anteriores, también debe encargarse de eliminarlo de la matriz. De lo contrario, el recolector de basura no lo eliminará automáticamente.



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);
 });
}
      
      





Al eliminar un elemento de la matriz, está actualizando su contenido con la lista de elementos de la página.



Dado que cada elemento de la casa tiene una referencia a su padre, esto evita referencialmente que el recolector de basura libere la memoria ocupada por el padre, lo que conduce a fugas.



En el residuo seco



El artículo describe la mecánica general de la asignación de memoria, y el autor mostró qué problemas pueden surgir y cómo tratarlos. Todo esto es importante para cualquier desarrollador de Java Script.



, Frontend- Skillbox:



, - ( SPA — Single Page Applications).



, “ ” — ( , ), , , .



— . .



, ( , , ). , , “” — garbage collector.



- (js , garbage collector’a). , .



All Articles