Interview questions about the JS memory mechanism often come right after closures. Many times, interviewers will throw out questions like “The relationship between closures and memory leaks.” Such questions are easy to answer in a few lines if they are purely factual. Most of the time, however, interviewers are prepared to dig into deep questions about JS memory management, garbage collection, and the causes of memory leaks. From my personal experience, every time I ask this kind of question, I can beat a lot of students who are not solid enough. We gouge from the ribs, this string of knowledge link stroke thoroughly, let the interviewer dig no dig.

JS memory life cycle

Memory management is a basic capability that every programming language has.

The difference is that some languages leave this capability open — such as the MALloc () and free() methods in C, which expose memory management to developers.

In other languages, such as JS, this ability is “hidden” : JS does not expose any memory operations to the developer, but silently automates all administrative actions itself. This is the reason why JS memory management is not taken seriously by most students.

The MEMORY life cycle of JS, like most programming languages, is divided into three phases. The three stages are well understood, similar to the process of farming:

  • “Dig a hole” — carve out your own space in the vast, fertile land of memory space. This is called “allocating memory”.
  • “Pit” – “plant” your space: stuff it with information you need to store. You can then read it and change it, a move called “read and write to memory.”
  • “Also pit” — with pit for a while cool, but as a good citizen, we have to use this land on time to the village. The act of giving it back is called freeing memory.

In this way, we must be familiar with the “farming” general life cycle process. But by knowing the process, you may not be able to plant good fruit. In order to become well-off, you also need to know the other door in this “digging” stage — different fruit (data), it likes different soil, we must start from the type and characteristics of each “fruit”, open up different types of memory space for it.

Stack memory vs. heap memory

Base and reference types

JS data types, as a whole, have only two types: primitive types and reference types.

The basic types include Sting, Number, Boolean, NULL, undefined, Symbol. The most obvious feature of this type of data is that it is fixed in size, light in volume, and relatively simple. It is stored in the JS stack memory.

The only data types left are reference types, such as Object, Array, Function, and so on. This type of data is complex, occupies a large space, and has variable sizes. It is stored in the JS heap memory.

Diagram heap and stack

As you know, heap and stack are different data structures. A stack is a linear table, and a heap is a tree.

To understand stack memory from the perspective of computer theory is relatively unsophisticated. In fact, as long as you understand how primitive types and reference types actually exist in the memory world, this piece of knowledge won’t confuse you. To do this, let’s go straight to an example:

let a = 0; 
let b = "Hello World" 
let c = null; 
let d = { name: 'take it' }; 
let e = ['take it'.'Ming'.'bear'];
Copy the code

The first question is: which storage mode do the above five variables correspond to?

Based on what we have just said, it is not difficult to analyze the answer according to the following ideas:

A -NumberType -- Base type -- stack memory B --StringType -- Base type -- stack memory C --nullBase type stack memory dObject-- Reference type -- heap memory e --Array-- Reference type -- heap memoryCopy the code

Second question: what are the differences in access mechanisms among the five variables mentioned above?

This question seems to be asking about variables, but is actually asking about memory. Here you need to understand the JS stack storage characteristics. The shape of the five variables in memory is shown in the figure below:

During a visit toA, B, CWith three variables, the process is very simple:Gets the value of the variable directly from the stack.

When accessing D and E, there are two steps:

  1. Gets a reference to the variable’s object from the stack (its address in heap memory)
  2. Take the address obtained in 1, and then go to the heap memory space query, to get the data we want

Garbage collection mechanism

In this section, we will look at the action of releasing memory, which is also called “pit”.

Every once in a while, the JS garbage collector “inspects” variables. When it determines that a variable is no longer needed, it frees the memory occupied by the variable, a process called garbage collection.

So how does JS know if a variable is not needed? This leads to another focus on memory management – garbage collection algorithm.

In JS, there are two garbage collection algorithms we discuss – reference counting and tag scavenging.

Reference counting method

This is the most rudimentary garbage collection algorithm, and it’s pretty much obsolete in modern browsers, but there are still some interviewers who insist on asking the idea of this method in order to determine if your JS knowledge is comprehensive and deep enough. Therefore, we cast a brick to attract jade, first this has been out of the method to talk with you, by the way, together to see how it is out.

In JS, we emphasize that “reference” is used only to describe the memory address of the reference type. But for those of you with some JAVA background, you’ll know that a reference can be thought of as describing the memory address of the block of memory in which a variable is located — and that “variable” is a general concept that refers to all types of variables, not just any one type. In reference counting, the subject of a reference is all the entities in the JS environment.

When we point to a value with a variable, we create a “reference” to that value:

const students = ['take it'.'Ming'.'bear']
Copy the code

In the picture, you know that the assignment expression reads from right to left. So this line of code starts by opening up a chunk of memory and putting the array on the right into it, which now takes up a chunk of memory. The STUDENTS variable then points to it, creating a “reference” to the array. At this point, the array reference count is 1 (figure below).

In reference counting, there is a reference count for every value in memory. When the garbage collector senses that a value has a zero reference count, it determines that it is “useless” and the memory is freed.

For example, if we point students to null:

students = null
Copy the code

The reference count for [‘xiuyan’, ‘xiaoming’, ‘Bear ‘] will then go to zero, and it will become a piece of useless memory that will soon be recycled as “garbage”.

What’s so bad about reference counting?

The limitations of reference counting are more important than reference counting itself.

Here’s an example:

function badCycle() {
  var cycleObj1 = {}
  var cycleObj2 = {}
  cycleObj1.target = cycleObj2
  cycleObj2.target = cycleObj1
}

badCycle()
Copy the code

We’ve done badCycle. As you know, the life of a function scope is very short, and when the function is finished executing, all variables in the scope are removed as “garbage”.

But if we use reference counting, then cycleObj1 and cycleObj2 will still be alive even after badCycle completes — because cycleObj2’s reference count is 1 (Cycleobj1.target), The reference count for cycleObj1 is also 1 (cycleobj2.target) (see figure below).

Yeah, reference counting doesn’t tell usA circular referenceThe scene under the “garbage”!

If you allow variables like cycleObj1 and cycleObj2 to wreak havoc on memory, memory leaks are the inevitable outcome.

Mark clearance

Considering the serious limitations of reference counting, all browsers have used the tag clearing algorithm since 2012. It can be said that tag cleanup is the standard garbage collection algorithm in modern browsers.

In the tag clearing algorithm, the criterion for determining whether a variable is needed is whether it is reachable.

This algorithm has two stages, marking stage and clearing stage respectively:

  • Marking phase: The garbage collector first finds the root object, which in the browser is Window; In Node, the root object is Global. Starting from the root object, the garbage collector scans all variables that can be reached through the root object, and these objects are marked as “reachable.”
  • Cleanup phase: Variables that are not marked as “reachable” are considered unwanted and the wave variables are cleared

Now let’s look at this code:

function badCycle() {
  var cycleObj1 = {}
  var cycleObj2 = {}
  cycleObj1.target = cycleObj2
  cycleObj2.target = cycleObj1
}

badCycle()
Copy the code

After badCycle completes, both cycleObj1 and cycleObj2 are identified as unreachable objects from the root object Window, and they are purged as expected. In this way, the problem of circular references is solved simply by being marked clean.

Closures and memory leaks

What is a memory leak?

The released variables (memory garbage) are not released, but still occupy the original memory, leading to a series of problems such as performance deterioration and system crash. This phenomenon is called memory leak.

Closures are not a disaster

During the interview process, memory leaks are raised by some interviewers with closures.

In reality, however, memory leaks caused solely by closures are extremely rare (unless you are extremely non-standard, but that’s not the problem with closures, it’s the code that’s wrong). We need to look at the real causes of memory leaks in other ways. But to make sure you don’t have any blind spots, let’s start with the memory “threat” of closures.

The interviewer may not ask you directly, “How do closures cause memory leaks?” (If he’s a serious engineer, he probably won’t ask), preferring to “look at the code for problems.” In the “problem” code, the most talked about is actually “theThing” problem. The following code has been around for a long time:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // reference to 'originalThing'
      console.log("Hey hey hey.");
  };
  theThing = {
    longStr: new Array(1000000).join(The '*'),
    someMethod: function () {
      console.log("Ha ha ha."); }}; };setInterval(replaceThing, 1000);
Copy the code

Think: What’s wrong with this code?

To figure this out, you need to know something about the V8 engine, especially this one: in V8, once different scopes are in the same parent scope, they share that parent scope.

Unused in this code is a closure that will not be used, but a someMethod that shares the same parent scope is a closure that is “reachable” (meaning available for use). Unused refers to originalThing, which causes the someMethod with which it shares scope to also indirectly reference originalThing. The result was that someMethod was “forced” to produce a continuous reference to originalThing, which, though meaningless and useless, could never be recycled. Not only that, but each originalThing setInterval changes its pointer (to the last assignment to theThing), causing an accumulation of unused originalthings that cannot be reclaimed, resulting ina serious memory leak.

Cause analysis of memory leakage

In addition to the above analysis of this situation, consider the following factors that may cause memory leaks:

  1. Global variable caused by “hand slip”
function test() {
  me = 'xiuyan'
}
Copy the code

When you write code in non-strict mode, writing me instead of var ME causes the ME to be silently mounted on the global object.

According to the garbage collection strategy we discussed earlier, the me variable, if declared by var, will disappear as a function scoped variable at the end of the function call — which is what we expect. But now it’s a global variable that can never be erased. When you have a lot of these variables, you get into trouble.

  1. Forget to clear setInterval and setTimeout

To implement polling, we use setInterval:

setInterval(function() {
    / / the function body
}, 1000);
Copy the code

Or chaining setTimeout:

setTimeout(function() {
  / / the function body
  setTimeout(arguments.callee, 1000);
}, 1000);
Copy the code

In the case of setInterval and setTimeout for chained calls, the work of timers can be said to be endless. When the function logic that timers contain is no longer needed, and we forget to manually clear timers, they remain in memory forever. So when using timers, it’s important to ask yourself: When am I going to kill this thing?

  1. Clean up improper DOM
const myDiv = document.getElementById('myDiv')

function handleMyDiv() {
    // Some logic related to myDiv
}

/ / use myDiv
handleMyDiv()

// Try "deleting" myDiv
document.body.removeChild(document.getElementById('myDiv'));
Copy the code

Some of you think that the last line of code is enough to delete the myDiv DOM. This is naive, because the myDiv variable’s reference to the DOM still exists, and it’s still a piece of “reachable” memory. This kind of DOM that you think is clean, but is actually alive, can cause unexpected memory problems when it accumulates.

Here we analyze the causes of memory leaks. In fact, there are countless other “dirty operations” that can lead to memory leaks, and they all have a common name — misoperations.

Yes, memory leaks are not a very sophisticated proposition. They are often caused by low-level errors. To put it bluntly, that is, the code is bad. Therefore, no matter in which stage of learning, students should maintain a rigorous attitude – in awe of the hands of the keyboard, awe of every line of code!