Personal Summary:
1. Memory recovery mechanism-tag clear algorithm: Starting from the root (global variable) to detect descendant variables, any root variables can not reach the variable will be marked as memory garbage.
This is the third chapter of how JavaScript works.
We will discuss another important topic that is increasingly overlooked by developers in everyday use, which is the pot of increasingly sophisticated and complex programming languages, memory management issues.
Overview
Like the C language has the underlying memory management primitives such as malloc()
and free()
. Developers use these primitives to explicitly allocate and free memory from the operating system.
At the same time, when creating things (objects, strings, etc.), JavaScript allocates memory and "automatically frees" memory when they are no longer in use, a process known as memory garbage collection. At first glance, the release of resources that essentially "automates the release of memory" is the cause of confusion and gives JavaScript (and other high-level language) developers a false impression that they can choose to ignore memory management. This is a huge mistake.
Even when using high-level languages, developers should understand memory management (or at least some basics). Sometimes there are problems with automated memory management (such as bugs in garbage collection or limitations of implementation, etc.), in order to be able to handle memory leaks properly (or to find a suitable solution with minimal cost and code flaws), developers must understand memory management.
Memory Life cycle
Regardless of which programming language you use, the memory life cycle is almost the same:
Here's an overview of what's going on in each step of the life cycle:
- Allocating memory-memory is allocated by the operating system so that it can be used by the program. In the underlying language (for example, C), developers can explicitly manipulate memory. In a high-level language, the operating system handles it for you.
- Use memory-This is the phase of memory allocated before the program actually uses it. When you use an assigned variable in your code, the memory read-write operation occurs.
- Free memory-This stage allows you to free up the entire block of memory that you no longer use, and the memory can be freed and reused. As with the memory allocation operation, the operation is also explicitly written in the underlying language.
For a quick tour of the concept of call stacks and dynamic memory management, you can read the first article.
What is memory?
Before jumping directly into JavaScript memory management, let's briefly introduce the memory and how it works.
From the hardware level, the computer memory is made up of a large number of flip flops (this is probably checked, that is, a large number of binary circuits). Each flip flop contains a small number of transistors and is capable of storing a bit. A single flip flops can be addressed by a unique identifier, so you can read and overwrite them. So theoretically, we can think of the whole computer's memory as being made up of a huge array of bits, so we can read and write.
As apes, we are not good at using bits for all logical thinking and calculations, so we organize bits into a larger group so that we can use them to represent numbers. 8 bits is called a byte. In addition to the byte, there is a word (16 or 32 bits).
There are many things stored in memory:
- All variables and other data used by all programs.
- program code, including the operating system code.
The compiler and the operating system work together to manage memory for you, but it is recommended that you understand how the underlying is implemented.
When compiling the code, the compiler checks the original data type and calculates in advance the amount of memory required for the program to run. In the so-called static stack space, the required memory size is allocated to the program. The memory in which these variables are allocated is called a static memory space because the memory required by the function is added to the top of the existing memory when the function is called. When the function breaks, they are moved out of memory in the order of LIFO (last in, first out). For example, consider the following code:
int n; // 4 字节int x[4]; // 4 个元素的数组,每个数组元素 4 个字节double m; // 8 字节
The compiler immediately calculates the required memory for the code: 4 + 4 x 4 + 8 = 28 bytes.
This is how the compiler handles the size of the current integer and floating-point numbers. About 20 years ago, integers were generally 2 bytes and floating-point numbers were 4 bytes. The code does not depend on the byte size of the current underlying data type.
The compiler inserts the markup, and the tag takes the required memory size from the stack and the operating system negotiates to store the variable in the stack.
In the example above, the compilation knows the exact memory address of each variable. In fact, when you write n
a variable, it is internally converted to something like "memory address 412763".
Notice here that when we try to access it x[4]
, we will have access to M-related data. This is because we have accessed an array element that does not exist in the array-it exceeds the last 4 bytes of the array element that was actually allocated to memory x[3]
, and there are bits that may be read (or overwrite) m
. This is almost certain to result in unintended consequences of other programs.
When the function calls other functions, each function gets its own shard memory address on the stack when it is called. The function will save all of its local variables, but there will also be a program counter to remember the address of the function in its execution environment. When the function is finished, its memory block can be used again for other purposes.
Dynamic memory allocation
Unfortunately, it's not as easy to know how much memory a variable requires at compile time. Imagine doing something like this:
int n = readInput(); // 从用户读取信息...// 创建一个含有 n 个元素的数组
Here, the compiler does not know how much memory is required by the compile-time array, because this is determined by the value of the array element entered by the user.
Therefore, it is not possible to allocate memory space for a variable in the stack. Instead, the program needs to explicitly allocate from the operating system to the correct memory space at run time. The memory here is allocated by the dynamic memory space. The difference between static and dynamic memory allocations is summarized in the table:
* The difference between static and dynamically allocated memory *
To fully understand how dynamic memory allocation works, we need to take some time to understand pointers, which may be a bit off the ^.^. If you are interested in pointers, please leave a message and we will discuss more about pointers in a later section.
Memory Allocations in JavaScript
Now, we'll show you how to allocate memory in JavaScript (the first step).
By declaring variable values, JavaScript handles memory allocation work by itself without the need for developer intervention.
var n = 374; // 为数字分配内存var s = ‘sessionstack‘; // 为字符串分配内存var o = { a: 1, b: null}; // 为对象及其值分配内存var a = [1, null, ‘str‘]; // (类似对象)为数组及其数组元素值分配内存function f(a) { return a + 3;} // 分配一个函数(一个可调用对象)// 函数表达式也分配一个对象someElement.addEventListener(‘click‘, function() { someElement.style.backgroundColor = ‘blue‘;}, false);
Some function calls also assign an object:
var d = new Date(); // 分配一个日期对象var e = document.createElement(‘div‘); // 分配一个 DOM 元素
Methods that can assign values or objects:
var s1 = ‘sessionstack‘;var s2 = s1.substr(0, 3); // s2 为一个新字符串// 因为字符串是不可变的,所以 JavaScript 可能会选择不分配内存而只是存储数组 [0, 3] 的内存地址范围。var a1 = [‘str1‘, ‘str2‘];var a2 = [‘str3‘, ‘str4‘];var a3 = a1.concat(a2);// 包含 4 个元素的新数组由 a1 和 a2 数组元素所组成
Memory usage in JavaScript
The use of allocated memory in JavaScript mainly refers to memory read-write.
You can use memory by assigning a value to a variable or an object property, or by passing a parameter to a function.
Free memory that is no longer in use
Most of the memory management problems are occurring at this stage.
The pain point is to detect when the allocated memory is idle. It often asks the developer to decide whether the memory in the program is no longer in use, and then releases it.
The Advanced programming language integrates a piece of software called the Garbage Collector, which works by tracking memory allocations and usage to find and automatically free up unused allocated memory fragments.
Unfortunately, this is an approximate process, as the general problem of determining whether some memory fragments are idle is its unpredictability (not solved by the algorithm).
Most garbage collectors collect memory that is no longer being accessed, such as all variables that reference the memory beyond the memory addressing range. However, there is still a memory space below the approximate value being collected, because in any case there may still be variables that refer to the memory address within the memory addressable range, even if the memory is idle.
Memory Garbage Collection
Because of the unpredictability of the "no-use" memory, garbage collection implements a limited solution to this common problem. This section will explain the necessary points of view to understand the main memory garbage collection algorithms and their limitations.
Memory reference
References are one of the main concepts that the memory garbage collection algorithm relies on.
In the memory management context, if object a accesses another object, B, indicates that a refers to object B (which can be implicit or explicit). For a chestnut, a JavaScript object has a reference to its prototype (an implicit reference) and its property value (explicit reference).
In this context, the concept of "object" is extended beyond the generic JavaScript object and contains the function scope (or global lexical scope).
Lexical scopes define how variable names are parsed in nested functions. Even if the parent function has returned, the internal function will still contain the scope of the parent function.
Garbage Collection Reference count
This is the simplest memory garbage collection algorithm. When an object is referenced by 0, it is marked as "recyclable memory garbage."
Look at the following code:
var o1 = { o2: { x: 1 }};// 创建两个对象。// ‘o1‘ 引用对象 ‘o2‘ 作为其属性。全部都是不可回收的。// ‘o3‘ 是第二个引用 ‘o1‘ 对象的变量var o3 = o1;o1 = 1; // 现在,原先在 ‘o1‘ 中的对象只有一个单一的引用,以变量 ‘o3‘ 来表示// 引用对象的 ‘o2‘ 属性。// 该对象有两个引用:一个是作为属性,另一个是 ‘o4‘ 变量var o4 = o3.o2;// ‘o1‘ 对象现在只有 0 引用,它可以被作为内存垃圾回收。// 然而,其 ‘o2‘ 属性仍然被变量 ‘o4‘ 所引用,所以它的内存不能够被释放。o3 = ‘374‘;o4 = null;// ‘o1‘ 中的 ‘o2‘ 属性现在只有 0 引用了。所以 ‘o1‘ 对象可以被回收。
Circular referencing is a nuisance.
Circular references can cause limitations. In the following example, two mutually referenced objects are created, which creates a circular reference. They will go out of scope after the function call, so in fact they are useless and can release references to them. However, the reference counting algorithm considers that because two objects are referenced at least once, they are not recyclable.
function f() { var o1 = {}; var o2 = {}; o1.P = O2; // O1 引用 o2 o2.p = o1; // o2 引用 o1. 这就造成循环引用}f();
Tag-Purge algorithm
To determine whether a reference to an object needs to be disposed, the algorithm determines whether the object is available.
The tag-purge algorithm consists of three steps:
- Root: In general, the root refers to the global variables referenced in the code. In the case of JavaScript, the Window object is the global variable of the root. The corresponding variable in node. js is "global". The garbage collector builds a complete list of all root variables.
- The algorithm then detects all the root variables and their descendant variables and marks them as active (indicating that they are not recyclable). Variables (or objects, etc.) that cannot be reached by any root variable are marked as memory garbage.
- Finally, the garbage collector releases all inactive memory fragments and returns them to the operating system.
Tag-Clear dynamic diagram of the algorithm
This algorithm is better than the previous algorithm because object 0 references can make the object inaccessible. Otherwise, as seen before, the circular reference.
Since 2012, all modern browsers have built in a tag-clear garbage collector. In previous years all optimizations for JavaScript memory garbage collection (generational/incremental/concurrent/parallel garbage collection) were optimized for the implementation of the tag-purge algorithm, but neither the garbage collection algorithm itself nor the ability to improve the decision object availability were raised.
You can view this article to learn more about tracking memory garbage collection and include an optimized markup-cleanup algorithm.
Circular references no longer make the egg ache.
In the first example earlier, when the function returned, the global object no longer references the two objects. As a result, the memory garbage collector finds that they are not available.
Even if two objects are referenced to one another, they cannot be obtained from the root variable.
Anti-intuitive behavior of the memory garbage collector
Although the memory garbage collector is convenient, they also have a number of costs. One of them is uncertainty. This means that memory garbage collection is not predictable. You cannot determine the exact timing of the memory garbage collection. This means that in some cases, the program will use more memory than is actually needed. In other cases, in a particular interactive sensitive program, you might want to be aware of those memory garbage collection short pause times. While uncertainty means that it is not possible to determine when memory garbage collection can be done, most GC implementations are the general pattern of memory garbage collection during memory allocation. If memory allocations are not made, most of the memory garbage collection will remain idle. Consider the following scenarios:
- Allocate a fixed amount of memory.
- Most of the elements (or all of them) are marked as unreachable (assuming we are assigning a value that we no longer need for the cache to be null)
- No additional memory is allocated.
In this scenario, most of the memory garbage collector no longer runs any memory garbage collection. In other words, even if the unreachable reference can be garbage collected, the memory collector is not tagged. While this is not a strict memory leak, it can result in higher than usual memory usage.
What is a memory leak?
As memory management says, memory leaks, which are used by some programs in the past but are idle, are not returned to the operating system or to an available pool of memory.
Programming languages like a variety of memory management methods. However, whether a memory fragment is being used is an indeterminate issue. In other words, only the developer knows if a memory fragment can be returned to the operating system.
Some programming languages provide developers with functional functions to solve this problem. Other programming languages depend entirely on the developer's full control over which memory fragments are recyclable. The encyclopedia has good articles on manual and automatic memory management.
Four common JavaScript memory leaks 1: Global variables
JavaScript handles undeclared variables in an interesting way: when referencing an undeclared variable, a new variable is created on the global object. In the browser, the global object is window
, which means the following code:
function foo(arg) { bar = "some text";}
Equivalent to:
function foo(arg) { window.bar = "some text";}
The variable bar
is meant to be referenced only in the Foo function. But if you do not var
declare the variable, an extra global variable will be created. In the above example, it does not cause a big accident. But you can naturally imagine a more destructive scenario.
You can also use this
keywords to inadvertently create a global variable.
function foo() { this.var1 = "potential accidental global";}// 调用 foo 函数自身,this 会指向全局对象(window)而不是未定义
You can avoid all of these problems by adding them at the top of the JavaScript file, ‘use strict‘
‘use strict‘
switching to a more restrictive JavaScript parsing mode, which prevents accidental global variables from being created.
Unexpected global variables are a problem, and code is often contaminated with explicitly defined global variables that are not collected by the memory garbage collector, as defined by the global variables. What you need to pay particular attention to is the use of global variables to temporarily store and process large bits of information. Use global variables to store data only when necessary, and remember to assign null or redistribute it once you are no longer using it.
2: Timer and forgotten callback function
Because it's often used in JavaScript setInterval
, let's take it for example.
The framework provides observers and other instructions that accept callbacks, often ensuring that all references to callbacks become unavailable when their instances are not available. It is easy to find the following code:
var serverData = loadData();setInterval(function() { var renderer = document.getElementById(‘renderer‘); if (renderer) { renderer.innerHTML = JSON.stringify(serverData); }}, 5000); // 这将会每隔大约 5 秒钟执行一次
The code snippet above shows the consequences of using timers to refer to nodes or data that are no longer needed.
renderer
objects are replaced or removed at some point, which can cause code that is encapsulated by the timer handler to become redundant. When this happens, either the timer handler or its dependencies are not garbage collected, because the timer needs to be stopped first (remember, the timer is still active). This can be attributed to the variables that are stored and processed for data loading and are serverData
not garbage collected.
When using observers, you need to make sure that you remove them explicitly once you no longer need them (no more observers or objects become unavailable).
Fortunately, most modern browsers will handle it for you: when the Observer object becomes unavailable, even if you forget to remove the event listener function, the browser automatically recycles the watcher handler. Previously, some old-fashioned browsers couldn't handle these situations (such as older IE6).
The best practice, then, is to remove the watcher handler when the object is discarded. See the following example:
var element = document.getElementById(‘launch-button‘);var counter = 0;function onClick(event) { counter++; element.innerHTML = ‘text‘ + counter;}element.addEventListener(‘click‘, onClick);// Do stuffelement.removeEventListener(‘click‘, onClick);element.parentNode.removeChild(element);// 现在当元素超出范围// 即使在不能很好处理循环引用的浏览器中也会回收元素和 onClick 事件
You no longer need to invoke a DOM node until it is unavailable, removeEventListener
because modern browsers support the memory garbage collector to detect and properly handle the life cycle of the DOM node.
If you use the jQuery
API (other libraries and frameworks also support the API), you can remove the event listener function before discarding the node. JQuery also ensures that even in older browsers, there is no memory leak.
Closed Package
Closures are an important feature of JavaScript: Nested functions can access variables of external (enclosing) functions. Due to the implementation details of the JavaScript runtime, the following methods may cause a memory leak:
var theThing = nullvar replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 引用 originalThing console.log("hi"); }; theThing = { longStr: new Array(1000000).join(‘*‘), someMethod: function () { console.log("message"); } };};setInterval(replaceThing, 1000);
When called replaceThing
, the object consists of theThing
a large array and a new closure ( someMethod
). The originalThing
closure that is unused
created by the variable is referenced (that is, replaceThing
the variable before the function is referenced theThing
). It is important to remember that when a closure scope is created for closures in the same parent scope, the closure scope is shared.
In such cases, closures someMethod
and unused
sharing are the same scope. unused
referenced by origintalThing
. Even if unused
you never use it, you can replaceThing
use functions outside of the scope someMethod
. Then someMethod
, because and unused
sharing the same closure scope, the unused
variable reference originalThing
is forced to unused
remain active (two closure sharing scopes). This will prevent memory garbage collection.
In the example above, closures someMethod
and unused
shared scopes are unused
referenced origintalThing
. Can be replaceThing
used in an extraterritorial scope theThing
someMethod
, even if it unused
has never been used. In fact, because of someMethod
and unused
shared closure scopes, the unused reference origintalThing
requires unused to remain active.
All of these behaviors can cause a memory leak. As you continue to run the code snippet, you will find a spike in memory utilization. These memory usage rates do not degrade when the memory garbage collector is running. This creates a closed list (the root variable is the current case theThing
), and each closure scope indirectly references a large array.
The problem was discovered by the Metor team and they wrote a good article detailing the problem.
4: From DOM reference
Sometimes, developers store DOM nodes in data structures.
Let's say you want to quickly update a few lines of table content. If you save a reference to each table row in a dictionary or array, this will result in a duplicate reference to the same DOM element: one in the DOM tree and the other in the dictionary. If you want to release references to these table rows, you need to remember to make these references inaccessible.
var elements = { button: document.getElementById(‘button‘), image: document.getElementById(‘image‘)};function doStuff() { elements.image.src = ‘http://example.com/image_name.png‘;}function removeImage() { // image 元素是 body 元素的直系后代元素 document.body.removeChild(document.getElementById(‘image‘)); // 这时,我们仍然在 elements 全局对象中引用了 #button 元素 // 换句话说,按钮元素仍然在内存中且不能够被垃圾回收器收集}
The additional consideration you need to take is to refer to the inner node or leaf node in the DOM tree. If you save a reference to a cell in your code, when you decide to remove the table from the DOM, you still keep a reference to that cell, which leads to a lot of memory leaks. You can assume that the memory garbage collector will release memory other than that cell. And it's not over yet. Because a cell is a descendant element of a table and the descendant element holds a reference to its parent node, a reference to a cell can cause the memory that is occupied by the entire table to be freed.
Memory Management experience
The following content is for personal original sharing. by March.
Guiding ideology
Minimize the memory footprint and reduce the GC as much as possible.
Reduce GC Count
The browser will not periodically recycle the garbage memory, called GC, unscheduled triggering, usually when the browser to request new memory, the browser will detect whether to reach a threshold and then trigger. In general, GC can be time consuming, and GC triggers can cause page lag and drop frames. Therefore, we should avoid the triggering of GC whenever possible. The GC cannot be triggered by code, but some browsers, such as Chrome, can manually click the CollectGarbage button on the DevTools-TimeLine page to trigger the GC.
Reduce memory consumption
Reduce memory footprint, avoid excessive memory usage/system lag, app flashback, and so on, especially on the mobile side. When memory consumption is high, the browser may trigger the GC frequently. As mentioned earlier, GC triggers can be avoided if you can avoid requesting new memory when requesting new memory.
Optimizing scenarios Using object pooling
Object Pool * * (English: Object pool pattern) is a design pattern. * * An Object pool contains a set of objects that have been initialized and can be used, and objects can be created and destroyed when required. The user of the pool can take objects from the pond, manipulate them, and return them to the pool when they are not needed, rather than destroying them directly. This is a special kind of factory object.
If the cost of initialization and instantiation is high, and there is a need for frequent instantiation, the use of object pooling can achieve significant performance gains in the case of a small number of instances per instantiation. The time taken to get an object from the pool is predictable, but the time required to create a new instance is indeterminate.
The above is excerpted from Wikipedia.
Using object pooling technology can significantly optimize memory consumption when objects are frequently created, but it is recommended that you make the following subtle optimizations in different usage scenarios.
Create on Demand
Create an empty object pool by default, create objects on demand, and run out of return pools.
Pre-created objects
If you create objects frequently, such as scrolling events, touchmove events, resize events, internal for loops, and so on during high-frequency operations, GC may be triggered. Therefore, in special cases, can be optimized to advance the creation of objects into the pool.
In high frequency situations, it is recommended to use interception/stabilization and task queue related techniques.
Timed release
Objects within the object pool are not garbage collected, and in extreme cases a large number of objects are recycled into the pool but not released, which is counterproductive.
Therefore, the pool needs to design timing/quantitative release of the object mechanism, such as the used capacity/maximum capacity/pool use time parameters such as timing to release objects.
Other optimization Tips
Avoid creating objects as much as possible, and avoid calling methods that create objects, such as Array.slice
,,, Array.map
Array.filter
string addition, $(‘div‘)
and ArrayBuffer.slice
so on, if not necessary.
Objects that are no longer used are manually assigned NULL to avoid problems such as circular references.
Using Weakmap
Production environment do not use console.log
large objects, including DOM, large arrays, ImageData, ArrayBuffer and so on. Because console.log
the objects are not garbage collected. See would console.log prevent garbage collection?
Design pages reasonably, create objects on demand/render pages/load pictures and more.
Avoid the following questions:
- To save things, request all data at once.
- To save things, render all data at once, then hide.
- To save things, load/render all pictures at once.
Use duplicate DOM, etc., such as reusing the same pop-up window instead of creating multiple.
As in the Vue-element framework, components such as popover/tooltip are used in tables to create M * N instances, which can be optimized to create only one instance, dynamically set location and data.
The ImageData object is the JS memory killer and avoids creating ImageData objects repeatedly.
Re-use ArrayBuffer.
Compress pictures, load images on demand, render images on demand, use appropriate picture sizes, picture formats, such as WebP format.
Image processing Optimization
Suppose you render a 100KB size, 300x500 transparent picture, roughly divided into three processes:
Loading pictures
Load the picture binary format into memory and cache, which consumes 100KB of memory & 100KB cache.
Decode picture
Decodes the binary format into pixel format, which occupies a width of * high * 24 (transparent 32 bits) bit-size memory, i.e. 300 * 500 * 32, approximately equal to 585 KB, where the contract is called pixel format memory. Personal guess at this point the browser recycles 100KB of memory that was created when the picture was loaded.
Rendering pictures
Rendering a picture through the CPU or GPU, or GPU rendering, is also uploaded to GPU memory, which is time consuming, determined by the image size/memory bit width, the larger the image size, the slower the upload time, and the more memory that is consumed.
Among them, older browsers such as Firefox recycle pixel memory is a late time, if a large number of images rendered memory consumption is too high.
PS: The browser will reuse the same image binary memory and pixel format memory, browser rendering images will be in the following order to obtain data:
Video memory >> pixel format Ram >> binary memory >> cache >> fetch from server. What we need to control and optimize is the size and recovery of binary memory and pixel memory.
To summarize, the memory consumed by the browser when rendering a picture is determined by the size of the image file memory, width, transparency, etc., it is recommended that:
Replace pictures with CSS3, SVG, Iconfont, and Canvas. A page that displays a large number of images, it is recommended to use Canvas rendering instead of using the IMG tag directly. Refer to JavaScript Image object, image rendering and browser memory two or three for details.
Compress images appropriately to reduce bandwidth consumption and image memory usage.
Use the appropriate picture size, that is, responsive images, output different size images for different terminals, do not use the original image to reduce the replacement ICON, such as some picture services such as OSS.
Use the appropriate image format, such as using the WEBP format. Detailed picture format comparison, use scenes and other suggestions to view the Web front-end picture limit optimization strategy.
Load on demand and render images on demand.
When preloading a picture, remember to assign the IMG object NULL, otherwise it will cause the picture memory to not be freed.
When the picture is actually rendered, the browser reads it again from the cache.
Assign the Off-screen IMG object as NULL,SRC to null, and urge the browser to reclaim memory and pixel-formatted memory in a timely manner.
Removes the non-viewable area picture and renders it again when needed. And on-demand rendering when combined with simple implementation, switch SRC and v-src can be.
How JavaScript works (JavaScript works) (iii) memory management and how to handle common memory leak problems in class 4