Overview
Assume that we have a container that can hold only 7 bottles but instead of using these bottles we carry more bottles from the market then the container is out of order as we are not using old bottles, same as with memory now, we understand with a real example, Imagine that you have a computer program that is designed to keep track of all of the tasks that you need to complete in a day. The program creates a new object in memory each time you add a new task to the list. However, the program fails to properly delete these objects when they are no longer needed, resulting in a memory leak. Over time, as you add more and more tasks to the list, the program will use more and more memory, eventually causing the computer to slow down or crash.
const tasks = [];
function addTask(task) {
tasks.push(task);
}
while (true) {
const task = prompt("Enter a task: ");
addTask(task);
}
Main memory is a place where the actual execution of a program takes place in a computer (i.e also known as RAM). Execution context may be held in heap and stack which is a type of memory described in the below section. Leaks are described by their name which is about causing leakage in resources.
A memory leak lowers the amount of memory that is available, which lowers the computer's performance. In the worst case scenario, eventually, too much of the available memory may be allocated, causing the system or device to entirely or partially, the program to crash, or the system to significantly slow down as a result of thrashing.
It's possible that memory leaks won't cause any problems or won't even be visible.
Simply said, a memory leak is a heap unsupervised block of memory that has been abandoned by the program but hasn't been collected by the garbage collector and returned to the operating system.
Modern operating systems release the typical RAM used by an application when it closes. As a result, a memory leak in software that executes for a brief period may go undetected and is rarely harmful.
For Node JS
As I mentioned above, when I described memory leaks, Now we will specifically talk about NodeJS memory leaks now. To do this, we must comprehend how NodeJS manages memory.
This means understanding how memory is managed by the JavaScript engine used by NodeJS. AS NodeJS uses the V8 Engine for JavaScript.
The two primary types of memory are stack and heap memory.
Stack:
Static data, such as method/function frames, primitive values, and object pointers, are kept in this location. The operating system controls this area (OS).
Heap:
V8 saves objects or dynamic data in this location. This is the biggest block of memory area and it's where Garbage Collection(GC) takes place.
Types of Memory Leaks
- Global resources
- Closures
- Caching
- Promises
- Timers
- Event listeners
Global resources
It is incredibly simple to add to global variables and resources in Node because of the way that JavaScript works as a language. They accumulate over time and eventually cause the application to crash if they are not removed.
function createGlobalVariables() {
leak1 = 'Leaks'; //assigning value to the undeclared variable
this.leak2 = 'Another Leaks'; // 'this' points to the global object
};
createGlobalVariables();
window.leak1;
window.leak2;
Solution
- Use global variables judiciously: Only use global variables when we need them, and make sure to release them when we're done. In particular, be careful about creating global variables that contain large objects or arrays, as these can consume a lot of memory.
- Use global variables in a controlled way: Instead of creating global variables directly, we can use a global registry to store and retrieve our global variables. This allows us to have more control over the lifecycle of our global variables and makes it easier to release them when we're done.
- Use a garbage collector: Node.js includes a garbage collector that can help to identify and release memory that is no longer in use. We can tune the garbage collector to better match the needs of our application, which can help to prevent memory leaks.
- Monitor the application's memory usage: Use tools like the Node.js process.memoryUsage() function or the heapdump module to monitor our application's memory usage. This can help us to identify areas of our code that may be causing memory leaks, so we can fix them.
- Use a linter: There are several linters available for Node.js that can help to identify potential issues with global variables, such as unused variables or variables that are never released. Using a linter can help us to catch these issues before they become a problem.
- To avoid such surprises, always write JavaScript in strict mode using the 'use strict'; annotation at the top of our JS file. In strict mode, the above will result in an error. When we use ES modules or transpilers like TypeScript or Babel, we don't need it as it's automatically enabled. In recent versions of NodeJS, we can enable strict mode globally by passing the - use_strict flag when running the node command.
Closures
Function-scoped variables will be cleaned up after the function has exited the call stack and if there aren't any references left outside of the function pointing at them. The closure will keep the variables referenced and alive although the function has finished executing and its execution context and variable environment are long gone.
function outer() {
const potentiallyHugeArray = [];
return function inner() {
potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
console.log('Hello');
};
};
const sayHello = outer(); // contains definition of the function inner
function repeat(fn, num) {
for (let i = 0; i < num; i++){
fn();
}
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray
// now imagine repeat(sayHello, 100000)
In this example, potentiallyHugeArray is never returned from any of the functions and cannot be reached, but its size can grow infinitely depending on how many times we call the function inner().
Solution
- Understand when the closure has been created and what objects are retained by it.
- Understand closure's expected lifespan and usage (especially if used as a callback).
Caching
Caching is one of the ways we can leak memory in JS. When we wish to speed up our applications, especially ones that require large mathematical computations that consume a lot of CPU resources, caching comes in quite handy. It prevents the re-calculation of values, especially pure values. Now, caching speeds up computing power at the expense of memory. The computed values are stored in memory and are promptly returned upon request without having to be recalculated.
If the cached values are never used, this in-memory caching may get out of control and cause memory leaks. Because their content cannot be collected, they will remain there idle consuming up valuable space.
Solution
A more proper and robust solution that doesn't store caches in the machine's memory is to use dedicated caching servers like Redis or Memcached.
Promises
A promise gives us a handle to a function that runs in the future so that we can interact with the output or run other program elements after it is finished. When processing promises, We must be kept in our mind that Promises get stored in memory and do not clear until they are either handled or rejected.
Solution
- Use the Promise.prototype.finally() method: This method allows us to specify a callback function that will be called when a promise is either fulfilled or rejected. We can use this method to release resources that are associated with the promise.
- Use the Promise.prototype.catch() method: This method allows us to specify a callback function that will be called if a promise is rejected. we can use this method to release resources that are associated with the promise.
- Using the Promise.race() method in combination with a time-out can help to prevent memory leaks in JavaScript by ensuring that the memory associated with a promise is released if the promise takes too long to fulfill or reject. Here's an example of how we can use the Promise.race() method with a time-out to prevent memory leaks:
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Timeout')), 1000);
});
Promise.race([longRunningPromise, timeout]).then(function(result) {
// longRunningPromise fulfilled or rejected before the time-out
}).catch(function(error) {
// longRunningPromise took too long to fulfill or reject, so it was rejected with the 'Timeout' error
});
In this example, longRunningPromise is a promise that takes a long time to fulfill or reject. The timeout promise is a time-out that will reject with an error if longRunningPromise takes more than 1000 milliseconds to fulfill or reject.
By using the Promise.race() method in this way, we can ensure that the memory associated with longRunningPromise is released if it takes too long to fulfill or reject. This can help to prevent memory leaks caused by long-lived promises.
It's important to note, however, that using the Promise.race() method with a time-out alone will not necessarily prevent all memory leaks in JavaScript. We should also make sure to properly manage our promises by fulfilling or rejecting them when appropriate, and by handling rejections and releasing resources when necessary.
Timers
The most popular method of stopping an object from being garbage collected is to reference it in the callback with a setTimeout or setInterval. The reference to the object from the timer's callback will remain active for as long as the callback is invocable if the recurring timer is set in our code.
Solution
- Use the clearTimeout() and clearInterval() functions: These functions allow us to cancel a timer before it completes. By using these functions to cancel timers that are no longer needed, we can prevent them from causing memory leaks. for example :
const timerId = setInterval(function(), 1000); // saving the interval ID
// doing something …
clearInterval(timerId); // stopping the timer i.e. if the button pressed
- Use the unref() and ref() methods: The unref() method allows us to specify that a timer should not keep the Node.js process running if it is the only thing keeping the process alive. The ref() method allows us to specify that a timer should keep the process running. By using these methods to manage the lifecycle of our timers, we can prevent them from causing memory leaks.
Event listeners
Event listeners can cause memory leaks in Node.js if they are not properly removed when they are no longer needed. This can happen because event listeners continue to consume memory as long as they are registered, even if they are not being used.
Here's an example of how an event listener can cause a memory leak:
const EventEmitter = require('events');
const emitter = new EventEmitter();
function leakyFunction() {
emitter.on('someEvent', () => {
// Do something when 'someEvent' is emitted
});
}
setInterval(leakyFunction, 1000);
In this example, the leakyFunction() function is registering a new event listener every second. Each event listener consumes memory, and since the event listeners are never removed, the memory associated with them is never released. This can cause the heap size to increase over time, leading to a memory leak.
Solution
- To prevent event listeners from causing memory leaks in Node.js, we should make sure to remove them when they are no longer needed. We can do this using the removeListener() or removeAllListeners() methods. By properly managing our event listeners, we can prevent them from causing memory leaks in our Node.js application. for example :
const EventEmitter = require('events');
const emitter = new EventEmitter();
function eventHandler() {
// Do something when the event is emitted
}
emitter.on('someEvent', eventHandler);
// …
emitter.removeListener('someEvent', eventHandler);
Monitor and test memory leaks
We can monitor or test memory leaks by taking dumps of memory usage of the Node.js process at a particular time. For this, we can use multiple methods. Here is the list of some of them:
- Process Manager
- PM2
- Clinic.js
- Prometheus
- Express Status Monitor
- Sematext Monitoring
Process Manager
We can use the process.memoryUsage() function to monitor the memory usage of our Node.js application. process.memoryUsage() function is a built-in Node.js function that returns an object containing information about the memory usage of the Node.js process. The object has the following properties:
- rss: The resident set size (RSS) of the process, in bytes. This is the amount of memory that is currently resident in RAM.
- heapTotal: The total size of the JavaScript heap, in bytes. This is the amount of memory that has been allocated to the heap, including both used and unused memory.
- heapUsed: The total size of the used memory in the JavaScript heap, in bytes.
- external: The total size of memory being used by C++ objects bound to JavaScript objects managed by the V8 engine, in bytes. We can set up a timer to periodically log the memory usage of the process. Here's an example of how we can do this:
const util = require('util');
setInterval(() => {
console.log(util.inspect(process.memoryUsage()));
}, 1000);
This code will log the current memory usage of the Node.js process every 1000 milliseconds (1 second). The output will include the rss, heapTotal, heapUsed, and external properties, as well as the values for each of those properties.
We can use this technique to monitor the memory usage of our Node.js application over time and to identify any trends or patterns that may indicate a memory leak. If we see the heap size or the resident set size (RSS) continually increasing, it could be a sign of a memory leak.
It's important to note that the process.memoryUsage() function only provides a snapshot of the memory usage of the Node.js process at a particular point in time. To get a more complete picture of our application's memory usage, we may need to take multiple snapshots over an extended period of time.
PM2
PM2 is a process manager for Node.js applications that can be used to monitor the memory usage of our application.
To monitor the memory usage of a Node.js application managed by PM2, we can use the pm2 monit command. This command will display real-time metrics for the application, including the current heap size and heap usage.
For example, to monitor the memory usage of an application managed by PM2, we can use the following command:
pm2 monit
This will launch a terminal window displaying current metrics for all of the PM2-managed applications. The metrics include the heap size and utilization in addition to additional data like CPU consumption and event loop delay.
Node.js application's memory usage may be tracked in real-time using the pm2 monit command, which can also be used to spot any trends or patterns that can point to a memory leak. It can be an indication of a memory leak if the heap consumption or size keeps growing.
It's crucial to remember that the pm2 monit command only offers a snapshot of the memory use of the Node.js application at a specific time.
It's crucial to remember that the pm2 monit command only gives us a snapshot of the Node.js application's memory usage at a specific moment in time. We might need to keep an eye on the metrics for a while in order to get a more complete picture of how much memory our application is using.
Clinic.js
A tool for analyzing performance problems in Node.js applications is called Clinic.js. It comes with a number of add-on tools that may be used to find and fix memory leaks among other performance problems.
We can use the clinic diagnostic - mem-leaks command to utilize Clinic.js to check for memory leaks in our Node.js application. This command will launch our program and gather information about how much memory it uses while monitoring for any indications of a memory leak.
If we wanted to execute Clinic.js on a script called app.js, for instance, we would provide the command:
clinic diagnose - mem-leaks - node app.js
If Clinic.js detects a memory leak in our application, it will provide detailed information about the cause of the leak and suggest possible ways to fix it.
Top comments (0)