What is a memory leak

There is a garbage collection mechanism in the engine, which is mainly for some programs no longer use objects, clean up the collection to free up memory.

Does the garbage collection mechanism recycle all objects (garbage) that are no longer used?

Actually engine although recycling against doing all kinds of optimization to ensure waste to recycle as much as possible, but is not to say that we can completely don’t care about this, we still want to take the initiative to avoid some code for engine do recycling operations, because not all useless object memory can be recycled, that when the object is no longer used memory, When it is not recycled in time, we call it a Memory leak.

Common memory leaks

We’ll take a look at some common cases that can cause memory leaks.

Improper closure

A closure is a function that is nested inside and returns a function. This is what most people think of as a closure, and well, it is. Let’s take a look at some of the descriptions in JS highlight books:

  • JavaScript advanced programming: Closures are functions that have access to variables in the scope of another function
  • JavaScript’s Definitive Guide: Technically speaking, all JavaScript functions are closures: they’re all objects, and they’re all associated with a scope chain
  • JavaScript you didn’t know: Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope

As described in the previous three books, the scope of closures is relatively broad. We will not bother with the definition of closures for the time being, but use the simplest closure example that everyone agrees on:

function fn1(){ let test = new Array(1000).fill('isboyjc') return function(){ console.log('hahaha') } } let fn1Child = Fn1 () fn1Child() copies the codeCopy the code

Is the above example a closure? Did it cause a memory leak?

This is a typical closure, but it does not cause a memory leak, because the function returned has no internal reference to fn1, that is, the test variable inside fn1 can be reclaimed.

function fn2(){ let test = new Array(1000).fill('isboyjc') return function(){ console.log(test) return test } } let Fn2Child = fn2() fn2Child() copies the codeCopy the code

Is the above example a closure? Did it cause a memory leak?

Obviously, it is also a closure, and because the return function contains a reference to the test variable in function fn2, test is not reclaimed, causing a memory leak.

So how to solve it?

After the function is called, the external reference relationship should be empty as follows:

function fn2(){ let test = new Array(1000).fill('isboyjc') return function(){ console.log(test) return test } } let Fn2Child = fn2() fn2Child() fn2Child = null Copies the codeCopy the code

“Reduce the use of closures, which cause memory leaks…”

Improper use of closures can cause memory leaks.

Implicit global variables

We know that JavaScript garbage collection is automatic, and every once in a while the garbage collector finds data that is no longer used and frees up the memory space it occupies.

Looking at global and local variables, local variables in a function are no longer needed after the function is finished, so the garbage collector recognizes and frees them. However, with global variables, the garbage collector has a hard time determining when they are not needed, so global variables are usually not collected. It is OK for us to use global variables, but at the same time we need to avoid some extra global variables, as follows:

Function fn(){test1 test1 = new Array(1000).fill('isboyjc1'); Create implicit global variable test2 this.test2 = new Array(1000).fill('isboyjc2')} fn() copy codeCopy the code

Calling function fn, because there is no statement and function of this problem has caused two additional implicit global variables, the two variables will not be recycled, this kind of situation we want to avoid, we can use in the development of strict mode or through lint check to avoid the occurrence of these situations, thus reducing memory cost.

In addition, in our program, we also can not avoid the use of global variables, these global variables unless cancelled or redistribution is not recycled, it also requires our extra attention, that is to say, when we were in the use of global variables to store data, be sure to use after the empty or redistribution, of course, is very simple, Set it to NULL when you’re done using it, especially if you’re using global variables to continuously store a large amount of data in the cache. Remember to set the storage limit and clean it up in time, otherwise the amount of data will increase and the memory pressure will increase.

Var test = new Array(10000) // do something test = null Copy codeCopy the code

Free DOM reference

For the sake of performance or code brevity, we use variables to cache references to DOM nodes when we do DOM in our code, but when we remove the node, we should release cached references synchronously, otherwise the free subtree cannot be freed.

<div id="root"> <ul id="ul"> <li></li> <li></li> <li id="li3"></li> <li></li> </ul> </div> <script> let root = document.querySelector('#root') let ul = document.querySelector('#ul') let li3 = document.querySelector('#li3') // Root. RemoveChild (ul) {ul = null; ul = null; At this point, GC li3 = null </script> copies the codeCopy the code

As shown above, when we use variables to cache the DOM node reference and then delete the node, if we don’t empty the variable referenced in the cache, we still can’t GC and we will have a memory leak.

If we empty the parent node, but the references of the deleted parent node and its children are also cached in variables, then the whole free node tree under the parent DOM node tree cannot be cleaned, and there will still be memory leakage. The solution is to empty the variables that reference the child node, as shown in the following figure:

Timer for forgetting

We often use timers in programs, namely setTimeout and setInterval. Let’s start with an example:

// let someResource = getData() setInterval(() => {const node = document.getelementById (' node ') if(node) { Node.innerhtml = json.stringify (someResource))}}, 1000) copies the codeCopy the code

The above is a small example of my random copy, which puts the data into Node every second, but neither the variables in the callback function nor the callback function itself can be recycled until the setInterval ends.

What is the end? That is, clearInterval is called. If it is not cleared, it causes a memory leak. Not only that, but if the callback function is not recycled, then the dependent variables inside the callback function cannot be recycled either. So in the above example, someResource cannot be recycled.

SetTiemout also has the same problem, so when interval or timeout is not needed, it is best to call clearInterval or clearTimeout to clear it. RequestAnimationFrame in the browser also has this problem and we need to cancel using the cancelAnimationFrame API when we don’t need it.

Forgotten event listeners

When the event listener component mounted inside the related event handler, and not take the initiative to remove it when the component destroyed, which is the variable or function is considered need not recycle, if internal reference variable stores a large amount of data, may cause the page too much memory, thus causing unexpected memory leaks.

Use the Vue component as an example. React does the same thing:

<template> <div></div> </template> <script> export default { created() { window.addEventListener("resize", this.doSomething) }, beforeDestroy(){ window.removeEventListener("resize", this.doSomething) }, methods: {doSomething() {// do something}}} </script> Copy the codeCopy the code

The forgotten listener pattern

Listener mode We all know, whether it is Vue, React or others, that listener mode is common for some message communication in current front-end development frameworks, such as EventBus..

When we implement the listener pattern and the related components mounted inside the event handler, and not take the initiative to remove it when the component destroyed, one of the reference variable or function is considered need not recycle, if internal reference variable stores a large amount of data, may cause the page too much memory, this can cause unexpected memory leaks.

Using the Vue component as an example is simpler:

<template> <div></div> </template> <script> export default { created() { eventBus.on("test", this.doSomething) }, beforeDestroy(){ eventBus.off("test", this.doSomething) }, methods: {doSomething() {// do something}}} </script> Copy the codeCopy the code

As above, we simply clear it in the beforeDestroy component destruction life cycle.

Forgotten Map and Set objects

When a Map or Set Object is used to store objects, both objects are strongly referenced. If a Map or Set Object is not referenced, the memory will not be reclaimed automatically.

If Map is used, WeakMap can be used when the key is an object. WeakMap object is also used to save the key value pair, which is weak reference for the key (Note: WeakMap is a weak reference only for key), and must be an object, and the value can be any object or original value, because it is a weak reference to the object, it will not interfere with Js garbage collection.

If you need to use Set reference object, you can use WeakSet, WeakSet object allows to store the unique value of weak reference object, the value in WeakSet object also will not repeat, and can only save the weak reference object, also because it is a weak reference to the object, will not interfere with Js garbage collection.

May need a simple introduction here, talk about a weak reference, our strong reference for first, before we say that the garbage collection mechanism of JS is if we hold on an object reference, then the object will not be garbage collected, the reference here, means the strong reference, and if it is only a weak reference is an object reference by a weak reference, Is considered inaccessible (or weakly accessible) and therefore may be reclaimed at any time.

Don’t understand? Just look at an example:

// let obj = {id: 1} // let obj = {id: 1} // let obj = {id: 1Copy the code

The above is a simple way of overriding the reference to clear the object reference and make it recyclable.

Now look at this:

let obj = {id: 1} let user = {info: Obj} let set = new set ([obj]) let map = new map ([[obj, 'hahaha']]) obj = null console.log(user.info) // {id: 1} console.log(set) console.log(map) copies the codeCopy the code

{id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} {id: 1} 1}, we want to clear that so we have to override all references to make it null.

Next, we look at WeakMap and WeakSet:

let obj = {id: 1} let weakSet = new weakSet ([obj]) let weakMap = new weakMap ([[obj, 'hahaha']]) // Override obj reference obj = null 1} The copied code will be removed from memory in the next GCCopy the code

As shown above, WeakMap is used and WeakSet is weak reference. After setting obj reference to NULL, object {id: 1} will be cleaned out of memory in the next GC.

Uncleaned Console output

In the process of writing code, certainly can’t avoid some output, in a small team might not clean up these projects launched in the console, but the console is also a hidden danger, at the same time also is easy to be ignored, we can see the console output data, because the browser save our information data output object references, This is why an uncleaned console can leak memory if it outputs objects.

So, we can use console output for debugging purposes in a development environment, but in a production environment, it’s important to clean up the output.

Maybe some students will find it incredible, even do not believe, here is an example, you can read the article just test yourself, you can save this code oh! (Read on to find out how to test it.)

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> <button id="click">click</button> <script> ! function () { function Test() { this.init() } Test.prototype.init = function () { this.a = new Array(10000).fill('isboyjc') console.log(this) } document.querySelector('#click').onclick = function () { new Test(); }}() </script> </body> </ HTML > copy codeCopy the code

Troubleshooting, locating, and repairing memory leaks

As mentioned in the beginning, after running for some time, the program will slow down and even crash. I don’t know why, so let’s go through the whole process of troubleshooting, locating and fixing memory leaks with an example.

Since we have mentioned several cases that can cause memory leaks, we will write a Demo using these cases to check whether there is a memory leak from the perspective of the browser. If there is a leak, locate the source and fix it.

First, let’s make up a memory leak example:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> <button id="click">click</button> <h1 id="content"></h1> <script> let click = document.querySelector("#click"); let content = document.querySelector("#content") let arr = [] function closures() { let test = new Array(1000).fill('isboyjc') return function () { return test } } click.addEventListener("click", function () { arr.push(closures()) arr.push(closures()) content.innerHTML = arr.length }); </script> </body> </ HTML > Copy the codeCopy the code

As shown above, this is a very simple example of a memory leak consisting of a misused closure.

First, we have a closures function. It is a closures function. The simplest closures are not to mention closures. Each click executes the closure twice and pushes its execution result into the global array ARR. Since the closure execution result is also a function and there is a reference to the original closure’s internal array test, Therefore, each element in the ARR array makes the test array object in the closure referenced by it unrecoverable. The number of elements in the ARR array also represents the number of closure references we have. Therefore, the more times the program clicks, the more push, the more memory consumption, and the more pages will be jammed.

In order to facilitate later observation, we will update the length data of the global array ARR to the page every time we click the button, that is, from 0, the page value increases by 2 every time we click the button.

Of course, this is an example of our own writing, as God we know what causes it, so now, forget this, let’s say this is one of our project program, the development is finished and delivered to the test, the test sister found that the page is getting slower and slower after constantly clicking the button in the program, and then mentioned the BUG.

As programmers, we must be: “refresh the page is not good, card refresh refresh!!”

B: well… Products and testing will not allow, a user first let us change.

Ok, then change, the first step is to find out what went wrong, what caused it, then we call this part of the troubleshooting phase.

Troubleshoot problems

Chrome’s developer tool, called the Chrome Devtool, is a powerful tool that helps you analyze performance, security, networking, and other aspects of your application. It also allows you to quickly locate problems, but most people are not familiar with it.

Since we are focusing on memory leaks in this article, we assume that the above program has checked all items except memory and everything is fine. Next, we will check the memory area.

First of all, we open the traceless mode of the browser, then open the program code of the web page to be checked, and then open the console. The whole program interface is very simple, as shown below:

Then we go to the Performance panel, formerly called Timeline, which is a powerful tool used by Chrome Devtool to monitor Performance metrics. It can record and analyze various events that occur during the life cycle of a website. We can use it to monitor and analyze various performance conditions in our program, including memory, as shown below:

To begin, be sure to check the Memory option (marked 5 above) before starting so that we can see the Memory analysis.

Click Start recording (tag 1) to record, and then clean up the GC by clicking on the trash can (tag 6).

Click on the click button 100 times and the value on the page should be 200. Click on the trash can again to trigger the GC manually.

Click the click button 100 times again and the value on the page should be 400, then stop recording.

Let’s look at the data panel generated by the console, as shown below:

The top two squares circled in red, the portion of the Heap that corresponds to it, represent periodic falls in memory, which is simply our memory.

We can obviously see that the memory data shows an increasing trend, and one might say that GC has not been executed yet. Don’t worry, remember when we clicked on the small garbage can at 200, which means we manually triggered the garbage collection once in the middle. We can use the page snapshot above to find out where the page value was at the moment when the page value was 200. It’s easy to hover over the memory snapshot and look for it, as shown below:

As you can see, even though we did a manual garbage collection in the middle, the amount of memory cleaned up did not decrease much, which leads us to conclude that the program’s click operation may be leaking memory.

OK, we’ve identified the problem, so the next step is to locate the source of the leak.

You might say, well, since you’ve figured out the problem is the click event, why not just go fix it?

You know, this is we write a simple example, we can see where the problem, but a click event in real project might exist a large number of operations, and we only know that the click event may lead to memory leaks, but don’t know the specific problem is on the click event which steps, cause more fine-grained and location we don’t know, All these need to be further analyzed to locate.

Analysis of the positioning

Next we start to analyze and locate the source of the leak

Chrome Devtool also provides a Memory panel that provides more detailed information, such as recording the details of JS CPU execution time, displaying the Memory consumption of JS objects and associated DOM nodes, and recording the details of Memory allocation.

Heap Profiling can take a snapshot of the current Heap and generate a file describing all the objects that JS is currently running on, how much memory they use, the hierarchy of references, and so on. It can be used to locate the specific cause and location of the problem.

Note that this is not the Memory panel under the Performance panel, but the same Memory panel as the Performance panel, as shown below:

So now it’s 400, or you could refresh the page and start at 0, so I’m going to continue

First click on the small garbage can (tag 3) to trigger a GC that removes useless items from memory

Click Start Snapshot (mark 1), create the first snapshot and select it as shown below:

Here’s a quick rundown of what the little picture above means:

Snapshot 1 in the list on the left represents Snapshot 1, which is the memory state at the moment we generated it

After Snapshot 1 is selected, the right view table has a drop-down box at the top left of the table with four values

  • Summary: Objects and their memory usage are captured, grouped by constructors, as a memory Summary that is used to track and locate memory leaks in DOM nodes
  • Comparison: Compares the difference between memory snapshots before and after an operation and analyzes the memory release before and after an operation to determine whether there is memory leakage and the cause
  • Containment: Probes the Containment heap and provides a view of the object structure for analyzing object references, closures, and deeper object analysis
  • Statistics: indicates the statistical view

By default, this drop-down selects Summary for us, so the table below shows a memory Summary of the data in snapshot 1.

So let’s take a quick look at the Summary option what do the columns in the table represent

  • Constructor: Displays all constructors. Click on each Constructor to view all objects created by that Constructor
  • Distance: Shows the Distance to the root node through the shortest node path, reference level
  • Shallow Size: Displays the memory occupied by the object, excluding the memory occupied by other objects that are internally referenced
  • Retained Size: Displays the total memory of the Retained object, including the memory of other internally referenced objects

OK, that’s enough for now, let’s go ahead, first click the garbage can to manually perform a GC, then click the Click button 1 below the page, and finally click the Create Snapshot button again to generate our second snapshot.

In order to be accurate, we will perform several more operations as follows:

First click the small garbage can to manually perform a GC, then click the Click button on the bottom of page 2, and finally click the Create Snapshot button again to generate our third snapshot

First click the small garbage can to manually perform a GC, then click the Click button on the bottom of page 3, and finally click the Create Snapshot button again to generate our fourth snapshot

Next, we select Snapshot 2 and change the drop-down box from the default Summary option to comparison, which compares the memory difference between the current snapshot and the previous snapshot, as shown below:

So let’s look at what the bottom column of the table represents when we select Comparison, and what are the important ones here

  • New: How many objects are created
  • Deleted: Indicates the number of objects reclaimed
  • Delta: Number of newly created objects minus number of reclaimed objects

We need to focus on Delta, as long as it’s a positive number, there may be a problem. The nice console has sorted it out for us, so we can look at the top ones in turn.

, of course, we also need to know the each line of data is representative of what is, attention to the Constructor of this column, we also said that one of them is a Constructor, each Constructor click can view all the objects created by the Constructor, or want to introduce this column in common Constructor is roughly represent

  • System and system/Context represent references that are created by the engine itself and created by the Context
  • Closures represent object references in some function closure
  • Array, String, number, and regexp are also examples of object types that reference arrays, strings, numbers, or regular expressions
  • HTMLDivElement, HTMLAnchorElement, DocumentFragment, and so on are all references to elements or specified DOM objects in your code

1-> 2/2 -> 3/3 ->4

Don’t worry. What are we gonna do now? We need to click on a single snapshot and select Comparison, and then look at the positive items listed in Delta and analyze them. This operation takes 3 times, because we have 4 snapshots, we need to compare and analyze them 3 times, and sometimes even more snapshots may be generated to ensure accuracy.

Is there an easier way? Yes, we can directly select the snapshot to compare, there is also a popup box on the right side of the table we can directly select the snapshot for comparison, and will also considerate filter out some useless information for us:

Let’s do the actual operation. Select Snapshot 2 on the left, and select snapshot 1 and snapshot 2 for comparative analysis. The results are as follows:

As you can see, there are only four differences left in the list after comparison filtering

System /Context we don’t have to worry about.

Closure represents a closure reference, so let’s click on it to see the details:

As you can see, there are two references under the closure, and it’s nice to point out that in line 21 of the code, click on one of the references, and there are more details below.

Why are there two references after expansion? Remember when we generated snapshot 2, a GC was manually executed and a click button was clicked, triggering a click event in which we executed and pushed the closure twice, so there were 2 records.

And finally array, the reference to array is only there because of the global array variable arr in our case code, after all, we push data every time we click, and that’s why we care about using global variables and cleaning them up in time, Because like this kind of situation you don’t clean up these global variables in have the page in memory, may everyone to have 2 items in the constructor columns are array in doubt, it is nothing wrong with, is a representative of arr itself, is a representative of the closure of internal reference array variable test (forget to review the case code above), This can also be got through the reference hierarchy represented in the Distance column, one level being 7 and one level being 8. We can also expand and select the reference entry to see the code location in detail. We won’t demonstrate the same operations as the closure above.

Check snapshot 4 on the left and compare snapshot 3 with snapshot 4. Before snapshot 4, GC was manually performed once and click click button three times. If the above conclusion is correct, We clicked the button three times and executed and pushed the closure function twice each time.

Well, at this point it seems clear that there are two problems: one is the memory leak caused by the closure reference array on line 21, and the other is the memory leak caused by the increasing number of elements in the global variable ARr.

The analysis is successful, go to the next step, repair and verify again.

Repair verification

Since this is a case written temporarily and there is no specific scene, there is no way to use a targeted way to repair it. So, this step is ignored for the time being, but we still need to solve it in the project.

For example, the global object keeps growing. We can’t avoid the global object, but we can limit the size of the global object, and clean up part of it if it can be larger than the scene.

For example, the problem with closure references, not allowing it to reference, or emptying after execution, which is all mentioned above.

In short, everything needs to choose the solution according to the specific scenario, after solving the problem, repeat the above troubleshooting process to see the memory.

Three large pieces of memory

There are actually three main problems with memory on the front end, which I affectionately refer to as the three big memory things:

Memory leaks we’ve been talking about for a long time, objects that are no longer in use but are not reclaimed, memory that is not released, that is, memory leaks, and to avoid that, avoid having useless data that still has references, that is, pay attention to the common memory leaks we mentioned above.

Memory bloat is when the memory usage increases rapidly and reaches a peak in a short period of time. To avoid the need to use technology to reduce memory usage.

Frequent GC, as the name indicates, occurs when large temporary variables are frequently used, causing the new generation space to fill up at a very fast rate, and GC is triggered every time the new generation fills up. Frequent GC also causes the page to stall. To avoid this, do not make too many temporary variables. This is because temporary variables are recycled when they are not used, which conflicts with memory leaks. In fact, as long as the degree is not too excessive, it is OK to avoid using global variables.

By Isboyjc Link: juejin.cn/post/698418… The copyright belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.