Memory Management and Garbage Collection

A Tale of C and JavaScript

Memory Management and Garbage Collection

Recently, I found myself concerned with memory leaks while developing a React project, and I was transported back to my C coding days.

If you're like me, you transitioned from C to JavaScript and couldn't help but wonder, "Wait, do I still have to think about memory in a language that practically serves me coffee?" (Ok, it’s chicory tea in my case, but you see my point, right?) ☕ ☕ ☕

Turns out, no matter the programming landscape, memory management is a constant companion. The difference is that in C, you're very hands-on, while in JavaScript, a lot of it is managed for you. Let's have a look!


Memory Management in C

In C, developers explicitly manage memory using functions like malloc() or calloc() to allocate memory, and free() to .. well, free the memory. When you forget to free the memory you've allocated, memory leaks happen.

These unreleased chunks of memory accumulate, leading to decreased performance and eventually causing the application or even the system to crash if the leak is severe enough.

For instance, in one of my early C projects, I had to allocate memory for storing image data, and then release it when it was no longer needed:

// Get image's dimensions
int height = abs(bi.biHeight);
int width = bi.biWidth;

// Allocate memory for image
RGBTRIPLE(*image)[width]=calloc(height, width*sizeof(RGBTRIPLE)); ⬅️
if (image == NULL)
{
   printf("Not enough memory to store image.\n");
   fclose(outptr);
   fclose(inptr);
   return 7;
}

// ... (some operations like filters on the image)

// Free memory for image
free(image); ⬅️

So, in C, I had to:

✅ Allocate memory to store an image using calloc(). If the allocation fails, an error message is displayed, and the program is terminated.

✅ Later, free this memory using free(image). Failure to do so would lead to a memory leak, which could eventually consume all the available memory.

Memory Management in JavaScript

In JavaScript, memory management is done automatically through a process called garbage collection, using a 'mark-and-sweep' algorithm.

✔️ When an object is created, memory is allocated to store it. During the 'mark' phase, all reachable objects are identified.

🧹 In the 'sweep' phase, the memory occupied by unreachable objects is freed. This means that if no variables or data structures in your program reference the object, there's no way to access that object's properties or methods. The object is considered unreachable and can safely be removed from memory. 🙌

It is the job of the garbage collector to free up unused memory. However, in certain scenarios, the garbage collector might not be able to identify unused memory, which again can lead to memory leaks.

For instance, if you accidentally declare a variable without the var, let, or const keyword, that variable becomes a global variable. You might think that the variable is destined for garbage collecttion after the function execution, but a global variable remains in memory for the lifetime of the application, potentially leading to a memory leak.

In this example, leakyVar is a global variable, even though it was declared in a function:

function leakyFunction() {
   leakyVar = 'This is a leaky variable'; 🚫
}

leakyFunction();

SO, what can we do to help garbage collection? 🤔

1. Don't Forget let, var, const!

Very unlikely, especially if you use strict mode or TypeScript, but theoretically still a possibility.

2. Manage Memory Explicitly

In React, for example, attaching event listeners can lead to memory leaks and require manual management. Let's look at an example where the useEffect hook allows us to manage such side effects:

useEffect(() => {
   const handleKeyDown = (event) => {
      if (event.key === 'Escape') {
         props.onClose();
       }
   };

   window.addEventListener('keydown', handleKeyDown); ⬅️      

   return () => {
      window.removeEventListener('keydown', handleKeyDown); ⬅️
   };
}, [props]);

Here, the useEffect hook is invoked whenever props change. We had to add an event listener for the keydown event, but also include a cleanup function that removes the event listener, essentially deallocating this resource and preventing a memory leak. The cleanup function is a crucial mechanism for resource deallocation, similar to free() in C.

3. Manual Dereferencing for Objects

Although JavaScript's garbage collector is typically efficient at cleaning up memory, there are situations where manual dereferencing might be helpful. Manual dereferencing is the process of intentionally removing references to an object so that it can be collected. For instance, when you're dealing with large data structures that won't be used again you can use manual dereferencing. This is done by setting the object to null.

Here is an example of manual dereferencing:

let box = {
   largeData: new Array(7000000),
   prop: 'some value'
};

Later in the code, when you know you're done with the object

// Manually dereference to free up memory sooner
box = null;

After this line of code executes, the largeData array and the box object itself are ready to be garbage-collected.

4. finally and release

The finally block and the release method serve different purposes, but they can be used in tandem for better resource management.

The finally block is used as part of the try...catch...finally statement. The code inside will execute regardless of whether an exception was thrown or caught. This makes it a perfect place to put any cleanup code that must be executed, such as closing files or releasing resources.

The release method usually serves the purpose of freeing up a resource that was previously allocated. For example, you might acquire a file handle to read a file. Once you're done, you would call the release method to let the operating system know that you're done with the file handle, allowing it to be released.

function * g() {
   const handle = acquireFileHandle(); // acquire resource
   try {
       // some code
   } finally {
       handle.release(); // release resource
   }
}

In the code above, the finally block ensures that any resources acquired, such as file handles, are properly released.

Please note that the * denotes that g is a generator function. A generator function is a special type of JavaScript function that can pause its execution, allowing it to return multiple values over time. This is particularly useful for tasks that don't complete immediately and require some form of waiting, like reading a file. In this example, it's important to note that the finally block within the generator function g will not execute until the generator is explicitly closed. This is usually done by calling the return() method on the generator object. So, the complete code is:

function * g() {
   const handle = acquireFileHandle(); // create resource
   try {
        // some code
   }
   finally {
      handle.release(); // resource cleanup
   }
}

const obj = g();
try {
   const r = obj.next();
   // some code
}
finally {
   obj.return(); // triggers cleanup in 'g'
}

I borrowed the example above from the list of examples used by the ECMAScript committee (TC39) to demonstrate one of the current approaches to resource management in JavaScript.

This example is part of a proposal to actually introduce a new syntax that allows for both synchronous and asynchronous management of resources. It introduces the using keyword, which essentially allows you to tie a resource to a block scope, automating its cleanup when the block is exited.

The using Keyword in the TC 39 Proposal

This proposal regarding the using keyword is now in stage 3, the penultimate stage before the inclusion in the ECMAScript standard.

The keyword introduces a cleaner and more declarative approach. When you declare a resource with using, the resource is considered "block-scoped." This means that as soon as the execution goes out of the block in which it was acquired, the resource is automatically released.

Here's an example taken from the proposal:

function * g() {
   using handle = acquireFileHandle(); // block-scoped resource
} // cleanup

{
   using obj = g(); // block-scoped declaration
   const r = obj.next();
} // triggers cleanup in `g`

Notice how straightforward the process becomes!

✅ It reduces the likelihood of leaks due to forgotten resource release.

✅ Makes the code more readable and maintainable.

Finally, the End

And there you have it!

Memory management from C to JavaScript: when you move from manual transmission to automatic, but still have to steer to avoid the potholes!

🚗 ⚠️ 🕳️

Keep your hands on the wheel, and may your code be ever leak-free!

Resources

  1. Learn more about the Explicit Resource Management Proposal by TC39.

  2. For a deeper dive into JavaScript memory management, check out MDN's comprehensive guide.

  3. For a informative discussion on garbage collection, listen to the JavaScript Jabber Podcast:

    Things JavaScript Developers Should Know, Part 2, episode 485

Blog post originally published on corinamurg.dev.