Memory leaks are often very insidious, such as the following code can you tell where the problem is?

let theThing = null;
let replaceThing = function() {
  const newThing = theThing;
  const unused = function() {
    if (newThing) console.log("hi");
  };
  // Keep modifying references
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a"); }};// The output value gets larger and larger each time
  console.log(process.memoryUsage().heapUsed);
};

setInterval(replaceThing, 100);
Copy the code

If you can, welcome to join our wechat pay overseas team 😊

If you can’t see it right now, read this article.

The first half of this article will introduce some theoretical knowledge, and then an example of locating memory leaks, interested friends can directly take a look at this example.

The overall structure

From the figure above, we can see that node.js Resident Set is divided into two parts: heap and stack.

  • The heap
    • New Space/Young Generation: used for temporary storage of New objects, the Space is divided into two equal parts, the overall smaller, adoptedScavenge (Minor GC)Algorithm for garbage collection.
    • Old Space/Old Generation: used to store objects that live for more than two Minor GC periodsMark-sweep & Mark-Compact (Major GC)Algorithm for garbage collection, the interior can be divided into two Spaces:
      • Old pointer space: stores objects that have Pointers to other objects.
      • Old data space: Stored objects contain only data (no Pointers to other objects), such as strings moved from the new generation, etc.
    • Code Space: The only executable memory for Code segments (although large Code segments can also be stored in large object Spaces).
    • Large Object Space: used to store objects that exceed the limits of other Spaces (Page::kMaxRegularHeapObjectSize) (see thisV8 Commit), objects stored here will not be moved during garbage collection.
    • .
  • Stack: Used to store raw data types, as well as function calls on and off the stack.

The space of the stack is managed by the operating system, and developers do not need to care too much; The heap space is managed by the V8 engine, and it can be a memory leak due to code problems or a slow application due to garbage collection after a long run.

We can simply observe node.js memory usage with the following code:

const format = function (bytes) {
  return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
};

const memoryUsage = process.memoryUsage();

console.log(JSON.stringify({
    rss: format(memoryUsage.rss), // Resident memory
    heapTotal: format(memoryUsage.heapTotal), // Total heap space
    heapUsed: format(memoryUsage.heapUsed), // The heap space used
    external: format(memoryUsage.external), // Spaces associated with C++ objects
}, null.2));
Copy the code

External is the space associated with C++ objects, for example via new ArrayBuffer(100000); When applying for a Buffer, you can see the increase of the external space.

You can adjust the default size of the related space by setting the following parameters, in MB:

  • --stack_sizeAdjusting stack space
  • --min_semi_space_sizeAdjust the initial value of Cenozoic half-space
  • --max_semi_space_sizeAdjusted the maximum Cenozoic half space
  • --max-new-space-sizeAdjusted the Max of Cenozoic space
  • --initial_old_space_sizeAdjust the initial value of old generation space
  • --max-old-space-sizeAdjust the maximum value of old generation space

The more common ones are –max_new_space_size and –max-old-space-size.

The Scavenge avenge algorithm, the mark-sweep & Mark-Compact algorithm have been written on a number of sites, such as node.js memory management and V8 garbage collection.

A memory leak

Memory leaks can sometimes occur due to improper code, and there are four common scenarios:

  1. The global variable
  2. Closure reference
  3. event
  4. Cache explosion

Let me give you an example.

The global variable

Variables that are not declared with var/let/const are bound directly to Global objects (node.js) or Windows objects (browsers) and are not automatically recycled even if they are no longer used:

function test() {
  x = new Array(100000);
}

test();
console.log(x);
Copy the code

The output of this code is [<100000 Empty items>], and you can see that array X is still not freed after the test function runs.

Closure reference

Memory leaks caused by closures are often quite insidious, as in the following code. Can you tell where the problem is?

let theThing = null;
let replaceThing = function() {
  const newThing = theThing;
  const unused = function() {
    if (newThing) console.log("hi");
  };
  // Keep modifying references
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a"); }};// The output value gets larger and larger each time
  console.log(process.memoryUsage().heapUsed);
};

setInterval(replaceThing, 100);
Copy the code

Running this code you can see that the output used heap memory is getting larger and larger, and the key is that because in the current V8 implementation closure objects are shared by all internal function scopes in the current scope, That is, theThing. SomeMethod and unUsed share the same closure context, resulting in theThing. SomeMethod implicitly holding a reference to a previous newThing. So would form theThing -> someMethod -> newThing -> last theThing ->… LongStr: new Array(1e8).join(“*”) is executed every time the replaceThing function is executed, and it is not automatically reclaimed, resulting in a larger memory usage and eventually a memory leak.

There is a neat solution to this problem: by introducing a new block-level scope that separates the declaration and use of newThing from the outside, breaking the sharing and preventing circular references.

let theThing = null;
let replaceThing = function() {{const newThing = theThing;
    const unused = function() {
      if (newThing) console.log("hi");
    };
  }
  // Keep modifying references
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a"); }};console.log(process.memoryUsage().heapUsed);
};

setInterval(replaceThing, 100);
Copy the code

This is done by {… } forms a separate block-level scope and has no external references, so newThing is automatically reclaimed during GC. For example, if I run this code on my computer, it will output:

2097128 2450104 2454240... 2661080 2665200 2086736 // Garbage collection at this time releases memory 2093240Copy the code

event

Memory leaks caused by event binding are common in browsers. They are usually caused by the event response function not being removed in time, resulting in duplicate binding or not processing the event response function after the DOM element has been removed, such as the following React code:

Class class extends Test {componentDidMount() {window.adDeventListener ('resize', function() {// } render() { return <div>test component</div>; }}Copy the code

The
component listens for resize events when it is mounted, but does not handle the function when it is removed. If the
is mounted and removed frequently, it will bind many useless event listeners to the window, resulting in a memory leak. This problem can be avoided by:

class Test extends React.Component { componentDidMount() { window.addEventListener('resize', this.handleResize); } handleResize() { ... } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); } render() { return <div>test component</div>; }}Copy the code

Cache explosion

The memory cache of Object/Map can greatly improve program performance, but it is very likely that the size and expiration time of the cache are not well controlled, resulting in invalid data still cached in the memory, resulting in memory leaks:

const cache = {};

function setCache() {
  cache[Date.now()] = new Array(1000);
}

setInterval(setCache, 100);
Copy the code

In the above code, the cache is constantly set, but there is no code to release the cache, resulting in memory eventually overflowing.

If memory caching is necessary, it is highly recommended to use the NPM package lru-cache. You can set the cache expiration date and maximum cache space, and use the LRU elimination algorithm to avoid cache explosion.

Memory leak location operations

When memory leaks occur, they can be difficult to locate for two main reasons:

  1. When the program starts running, the problem is not immediately exposed. It takes a continuous period of time, or even a day or two, for the problem to recur.
  2. The error message is so vague that it can only be seenheap out of memoryError message.

In this case, you can use two tools to fix the problem: Chrome DevTools and Heapdump. Heapdump does exactly what it says – it takes a snapshot of the heap’s state in memory and imports it into Chrome DevTools to see what objects are in the heap, how much space they take up, and so on.

Let’s take a look at the memory leak example from the closure reference above. NPM install heapdump

// const heapdump = require('heapdump'); heapdump.writeSnapshot('init.heapsnapshot'); // let I = 0; // let theThing = null; let replaceThing = function() { const newThing = theThing; let unused = function() { if (newThing) console.log("hi"); }; TheThing = {longStr: new Array(1e8).join("*"), someMethod: function() {console.log("a"); }}; if (++i >= 1000) { heapdump.writeSnapshot('leak.heapsnapshot'); Process.exit (0); process.exit(0); }}; setInterval(replaceThing, 100);Copy the code

In line 3 and line 22, the snapshot in the initial state and the snapshot after 1000 loops are exported as init. heapSnapshot and leak.heapSnapshot, respectively.

Then open Chrome, press F12 to bring up the DevTools panel, hit The Tab in Memory, and Load the two snapshots one by one:

After the import, you can see on the left that the heap memory has increased significantly, from 1.7MB to 3.1MB, almost doubling:

Next comes the crucial step, click on the Leak snapshot and compare it to the init snapshot:

Two columns are circled in red on the right:

  • Delta: indicates the number of changes
  • Size Delta: Expresses the size of the change space

As you can see, the top two items with the biggest growth are concatenated String and closure, so let’s click through to see what they are:

SomeMethod closure context and the long concatenated string thething. longStr are the main causes of the memory leak. We can also click on the Object module below to see the relationship of the call chain more clearly:

It is clear from the figure that the memory leak was caused by the rapid growth of memory caused by the continuous call of newTHing -> closure context -> someMethod -> last time.

Refer to the article

  1. Visualizing memory management in V8 Engine
  2. Github – Example of memory leak
  3. Ali Node – Open Chrome DevTools correctly

If you are interested in what I write, please pay attention to my notebook, which will record some interesting things.