Common Memory Leak Scenarios

A memory leak refers to the situation where dynamically allocated heap memory in a program is not released due to negligence or an error, resulting in wastage of system memory, slow program execution, or even system crashes. A memory leak does not necessarily mean that the memory physically disappears, but rather that the application allocates a segment of memory and, due to a design error, loses control over that segment before releasing it, thereby causing memory wastage. To detect memory leaks, Chrome provides performance analysis tools such as "Performance," which allow for convenient monitoring of memory usage.

Memory Reclamation Mechanism

Low-level languages like C generally have low-level memory management interfaces, such as malloc() and free(). In JavaScript, memory is automatically allocated when variables are created and automatically released when they are no longer in use. Among the seven basic types in JavaScript, objects, which are reference types, occupy a large and variable-sized memory space in the heap and have actual object storage in the heap memory and pointer storage in the stack memory. Access to objects is done through reference. Variables in the stack area are accessed by value and are destroyed when their scope is terminated. However, heap variables accessed by reference may still be referenced in an outer scope or another scope even after their original scope is terminated. They cannot be directly destroyed. In such cases, algorithms are used to determine whether the heap variable is no longer needed, thus deciding whether memory reclamation is necessary. JavaScript mainly uses two garbage collection algorithms: reference counting and mark-and-sweep.

Reference Counting Algorithm

In the reference counting garbage collection algorithm, whether an object is no longer needed is simplified to whether other variables or objects reference it. If there are no references pointing to an object, the garbage collector reclaims it. Here, an object refers not only to JavaScript objects, but also to function scope or global lexical scope. The reference counting garbage collection algorithm is used less frequently and is mainly used in older versions of Internet Explorer, such as IE6 and IE7.

var obj = {
    a : {
        b: 11
    }
}
// At this point, two objects are created: one is referenced as the "a" property of the other (object 1), and the other is referenced by the variable "obj" (object 2)
// Both objects have variables referencing them, so neither can be garbage collected

var obj2 = obj;
// At this point, object 2, which is referenced by "obj," is also referenced by "obj2"

obj = null;
// The reference from "obj" to object 2 is released, but object 2 still has a reference from "obj2"

var a2 = obj2.a;
// References object 1, so now object 1 has two references: "a" and "a2"

obj2 = null;
// One reference to object 2 is released, so now object 2 has no references and can be garbage collected
// The reference to object 1's "a" property is also released, so now only "a2" references it

a2 = null;
// The reference from a2 to object 1 is released, and now object 1 can be garbage collected

However, the reference counting garbage collection algorithm has a limitation: it cannot handle memory leaks caused by circular references.

function funct() {
    var obj = {}; // named as object 1, at this point the reference count is 1
    var obj2 = {}; // named as object 2, at this point the reference count is 1
    obj.a = obj2; // obj's "a" property references obj2, so now object 2 has a reference count of 2
    obj2.a = obj; // obj2's "a" property references obj, so now object 1 has a reference count of 2
    return 1;
    // After this function is called, the variables "obj" and "obj2" in the execution stack are destroyed, reducing the reference count of object 1 and object 2 by 1
    // Now, object 1 has a reference count of 1 and object 2 has a reference count of 1, so neither will be garbage collected
}

funct();
// Two objects are created and reference each other, forming a cycle. After they are called, they are no longer needed and can be collected, but the reference counting algorithm does not collect them because each has at least one reference.

Mark-and-Sweep Algorithm

In the mark-and-sweep garbage collection algorithm, whether an object is no longer needed is defined as whether the object can be reached. This algorithm sets a root object, which in JavaScript is the global object. The garbage collector periodically starts from the root and finds all objects referenced from it, and then recursively finds objects referenced by those objects. Starting from the root, the garbage collector identifies reachable and unreachable objects. This effectively solves the problem of circular references. All modern browsers use the mark-and-sweep garbage collection algorithm, and any improvements made to JavaScript garbage collection algorithms are based on this approach.

  • When the garbage collector runs, it marks all variables stored in memory.
  • Then, it removes the marks from variables in the execution environment and variables referenced by the execution environment.
  • After this, the variables still marked are considered ready for deletion, because they are no longer accessible from the execution environment.
  • Finally, the garbage collector completes the memory cleanup by destroying the marked values and reclaiming the memory space they occupy.

Common Memory Leak Scenarios

Unexpected Global Variables

In JavaScript, the handling of undeclared variables is not strictly defined. Even within a local function scope, global variables can still be defined. These unexpected global variables may store a large amount of data and, because they can be accessed through global objects such as window, they are not considered for memory collection during garbage collection. They persist until the window is closed or the page is refreshed, causing unexpected memory leaks. In strict mode, this unexpected global variable declaration will throw an exception in JavaScript. Furthermore, you can use eslint to perform prechecks for this kind of state. In fact, defining global variables is not a good practice. If you must use global variables to store a large amount of data, make sure to set it to null or redefine it after use. One of the main reasons for increased memory consumption related to global variables is caching. Cached data is meant for reuse and must have a size limit to be useful. High memory consumption leads to the cache exceeding its limit because the cached content cannot be reclaimed.

function funct(){
    name = "name";
}
funct();
console.log(window.name); // name
delete window.name; // If not manually deleted, it will persist as long as the window is not closed or refreshed

Neglected Timers

Timers like setInterval must be cleared in a timely manner, as otherwise the variables or functions referenced within may not be collected due to being considered necessary. If the internally referenced variables store a large amount of data, it can lead to excessive memory usage, thus causing unexpected memory leaks.

<template>
  <div></div>
</template>

<script>
export default {
  created: function() {
    this.refreshInterval = setInterval(() => this.refresh(), 2000);
  },
  beforeDestroy: function() {
    clearInterval(this.refreshInterval);
  },
  methods: {
    refresh: function() {
      // do something
    },
  },
}
</script>

Detached DOM Reference

Sometimes, it is useful to save the internal data structure of a DOM node, such as when you need to quickly update the content of several rows in a table, storing each row DOM as a dictionary or array makes sense. In this case, the same DOM element exists in two references: one in the DOM tree and another in the dictionary. In the future, if you decide to delete these rows, you need to clear both references. Additionally, you should also consider the issue of references within the DOM tree or its child nodes. If your JavaScript code saves a reference to a specific <td> in a table, and you later decide to delete the entire table, your intuition might lead you to believe that the GC will reclaim all nodes except the saved <td>. The actual situation is different. This <td> is a child node of the table, and due to the code retaining a reference to the <td>, the entire table remains in memory. Therefore, when saving references to DOM elements, caution is advised.

var elements = {
    button: document.getElementById("button"),
    image: document.getElementById("image"),
    text: document.getElementById("text")
};
function doStuff() {
    elements.image.src = "https://www.example.com/1.jpg";
    elements.button.click();
    console.log(elements.text.innerHTML);
    // more logic
}
function removeButton() {
    // The button is a descendant element of the body
    document.body.removeChild(elements.button);
    elements.button = null; // Clear the reference to this object
}

Closures

Closures are a key aspect of JavaScript development. They allow you to access the outer function scope from an inner function, essentially enabling access to another function's scope without necessarily implementing a scope chain structure within the function scope. Since closures carry the scope of the function that contains them, they can consume more memory than other functions. Overusing closures may lead to excessive memory consumption, and they need to be manually cleared after they are no longer needed.

function debounce(wait, funct, ...args){ // Debounce function
    var timer = null;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(() => funct(...args), wait);
    }
}

window.onscroll = debounce(300, (a) => console.log(a), 1);

The Forgotten Event Listener Pattern

When implementing the listener pattern and mounting relevant event handling functions within a component, if these are not actively cleared upon the component's destruction, the referenced variables or functions are considered necessary and will not be recycled. If the internally referenced variables store a large amount of data, it may lead to excessive memory usage and unexpected memory leaks.

<template>
  <div></div>
</template>

<script>
export default {
  created: function() {
    global.eventBus.on("test", this.doSomething);
  },
  beforeDestroy: function(){
      global.eventBus.off("test", this.doSomething);
  },
  methods: {
    doSomething: function() {
        // do something
    },
  },
}
</script>

The Forgotten Event Listener

When event listeners mount relevant event handling functions within a component, but are not actively cleared upon the component's destruction, the referenced variables or functions are considered necessary and will not be recycled. If the internally referenced variables store a large amount of data, it may lead to excessive memory usage and unexpected memory leaks.

<template>
  <div></div>
</template>

<script>
export default {
  created: function() {
      window.addEventListener("resize", this.doSomething);
  },
  beforeDestroy: function(){
      window.removeEventListener("resize", this.doSomething);
  },
  methods: {
      doSomething: function() {
        // do something
      },
  },
}
</script>

The Forgotten Map

When using Map to store objects, similar to detaching references from the DOM, if the references are not actively cleared, it will also lead to the memory not being automatically reclaimed. In cases where the key is an object, WeakMap can be used. WeakMap is used to save key-value pairs, with the key being a weak reference and must be an object, while the value can be any object or primitive value. Since it is a weak reference to the object, it does not interfere with JavaScript garbage collection.

var elements = new Map();
elements.set("button", document.getElementById("button"));
function doStuff() {
    elements.get("button").click();
    // more logic
}
function removeButton() {
    // button is a descendant of the body
    document.body.removeChild(elements.get("button"));
    elements.delete("button"); // clear the reference to this object
}

The Forgotten Set

When using Set to store objects, similar to detaching references from the DOM, if the references are not actively cleared, it will also lead to the memory not being automatically reclaimed. If we need to reference objects using Set, WeakSet can be used. WeakSet allows storing the unique values of weak references to objects, and values in the WeakSet cannot be repeated. It can only store weak references to objects and does not interfere with JavaScript garbage collection.

var elements = new Set();
var btn = document.getElementById("button");
elements.add(btn);
function doStuff() {
    btn.click();
    // more logic
}
function removeButton() {
    document.body.removeChild(btn); // button is a descendant of the body
    elements.delete(btn); // clear the reference to this object in the Set
    btn = null; // clear the reference
}

Daily Question

https://github.com/WindrunnerMax/EveryDay

References

https://zhuanlan.zhihu.com/p/60538328 https://juejin.im/post/6844903928060985358 https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/