Home About Me

Making Peace with JavaScript's Garbage Collector

JavaScript does not require you to release memory manually the way C or C++ does. Strings, arrays, and objects vary in size, so the interpreter allocates memory when each one is created. That memory must eventually be reclaimed; otherwise an application can consume all available memory and crash. JavaScript engines therefore include garbage collection. The engine detects when an object can no longer be used and releases its storage.

var a = "before";
var b = "override a";
var a = b; // overwrite a

After this runs, the string before has lost its reference because a was reassigned. The collector can reclaim that memory for reuse.

Two collection strategies

Browsers have generally relied on two approaches: mark-and-sweep and reference counting.

Mark-and-sweep

This is the most common form of garbage collection in JavaScript. When a variable enters an execution environment, it is marked as being in the environment. Logically, such a variable cannot be freed while execution may still touch it. When the variable leaves the environment, it is marked as out of the environment.

When the collector runs, it marks all variables stored in memory. It then removes marks from variables in the environment and from variables referenced by those environment variables. Anything still marked is considered ready for deletion, because it cannot be reached from the environment. Finally, the collector destroys those marked values and reclaims their memory.

Reference counting

Reference counting tracks how many references point to each value. When a variable is declared and assigned a reference-type value, that value has a reference count of 1. If the variable is assigned another value, the original value's count drops by 1. When the count reaches 0, the value can no longer be accessed and its memory can be reclaimed. The next time the collector runs, it frees values with a count of 0.

This approach has a problem with cycles:

function problem() {
  var objA = new Object();
  var objB = new Object();

  objA.someOtherObject = objB;
  objB.anotherObject = objA;
}

Here objA and objB reference each other through their properties, so each has a reference count of 2. After the function finishes, both objects have left scope, but their counts never reach 0. They remain in memory. If many such cycles exist, the result is a significant memory leak.

Old versions of IE implemented BOM and DOM objects as C++ COM objects, and COM uses reference counting for garbage collection. Even if the JavaScript engine used mark-and-sweep, JavaScript code that touched COM objects could still run into cycles. Consider this example:

var element = document.getElementById("some_element");
var myObj =new Object();
myObj.element = element;
element.someObject = myObj;

A DOM element and a native JavaScript object now reference each other: myObj.element points to element, and element.someObject points back to myObj. Because of the cycle, removing the DOM element from the page does not reclaim the memory. The cycle can be broken manually:

myObj.element = null;
element.someObject =null;

That prevents the leak.

Creating less garbage

The most obvious allocation is new Foo(). A better pattern is to create objects during initialization and reuse them afterward. Three other allocation expressions are less obvious:

  • {} creates a new object.
  • [] creates a new array.
  • function() {...} creates a new function, and creating a function also produces an object that garbage collection must handle.

Reusing objects

To maximize object reuse, avoid {} as you would avoid new. Object literals with properties are often returned from methods, which creates fresh memory on every call. One alternative is to put the returned data into a shared global object and return that object. Every call then mutates the same object, so the code needs clear comments and careful usage to avoid mistakes.

Another way to reuse an object is to delete its own properties and add new ones. cr.wipe does that:

// Delete every property of obj and turn obj into a fresh object efficiently!
cr.wipe = function (obj) {
  for (var p in obj) {
    if (obj.hasOwnProperty(p))
      delete obj[p];
  }
};

You can wipe an object, add new properties, and reuse it. Clearing an object this way is slower than creating a fresh literal with {}, but in realtime code the saved allocations can reduce garbage buildup and help avoid garbage collection pauses. That tradeoff can be worthwhile.

Reusing arrays

Assigning [] to an array is a quick way to clear it, but it also creates a new empty array and leaves the old array as garbage. Setting arr.length = 0 clears the array while reusing the same array object and producing less garbage.

Reusing functions

Methods are usually created during initialization and are rarely allocated dynamically at runtime. That makes dynamically created methods harder to spot, but it also gives you a clear target: anywhere a function is created at runtime, garbage may be produced. Returning a function is one example.

In a game loop, it is common to call a member method through setTimeout or requestAnimationFrame:

setTimeout( (function(self) { return function () { self.tick(); }; })(this), 16)

Every 16 milliseconds this calls this.tick(). At first glance it looks harmless, but each call returns a new function object, which creates a large amount of function garbage. Save the returned function instead:

// at startup
this.tickFunc = ( function(self) {
  return function() {
    self.tick();
  };
} )(this);

// in the tick() function
setTimeout(this.tickFunc, 16);

Instead of creating a new function object every time, each frame reuses the same one. The same idea applies wherever functions are returned or created at runtime.

Library calls and hot paths

JavaScript itself is built around garbage collection, and avoiding allocations becomes harder as you use libraries. Many convenient library methods allocate new objects. For example, array slice returns a new array, and string substr returns a new string. You can avoid calling such methods, or rewrite them in a way that does not create garbage, though that can be extreme.

In the Construct 2 engine, removing an array element by index was originally implemented like this:

var sliced = arr.slice(index + 1);
arr.length = index;
arr.push.apply(arr, sliced);

slice returns a new array object containing the elements after the removed index, and arr.push.apply copies them back into the original array. The sliced array then becomes garbage. Because this code ran very frequently in the engine, it was rewritten iteratively:

for (var i = index, len = arr.length – 1; i < len; i++)
  arr[i] = arr[i + 1];
arr.length = len;

Rewriting large numbers of library functions is painful, so weigh convenience against garbage production. If a method that allocates memory is called several times in every animation frame, a rewrite may be worth the effort.

Recursion has a similar issue. Creating {} at each step and passing data through the recursion is convenient, but a single array used as a stack is better. Push and pop data onto that array. To avoid garbage from pop, keep an index for the last element and decrement it instead; to avoid push, increment the index and only call the real push when that slot does not yet exist.

Vector objects such as vector2 with x and y properties are another common source of garbage. A method that returns a new vector can be convenient because it provides both values and can be modified by the caller, but creating hundreds of them in one frame can cause serious garbage collection problems. Prefer methods with separate responsibilities, such as getX() and getY(), over getPosition() returning a vector2.

Eliminating garbage collection in JavaScript is not realistic. Garbage collection and the timing requirements of realtime software are fundamentally opposed. Still, code can be audited for obvious cases of excessive allocation, and with attention it is possible to build realtime JavaScript applications with low garbage collection. For interactive games and applications, responsiveness and low garbage collection pressure both matter.