preface
JavaScript was born in 1995 and was originally designed for form validation within web pages.
JavaScript has grown so rapidly over the years that it has become one of the most popular development languages for programmers. And now JavaScript is no longer limited to the web side, has been extended to the desktop, mobile and server side.
With the advent of the big front end, more and more developers are using JavaScript, but many of them just “know it” and don’t know much about the language.
Understanding memory is a key point that can’t be ignored if you want to become a better JavaScript developer.
๐ This article mainly consists of two parts:
- JavaScript memory details
- JavaScript memory analysis guide
By the end of this article, you’ll have a good understanding of JavaScript memory and the ability to do memory analysis on your own.
๐ง Without further ado, let’s begin!
The article is a long one, with about 12,000 words excluding the code, and it will take some time to read, but I guarantee it will be well worth your time.
The body of the
Memory
What is Memory?
I believe you all have a certain understanding of the internal, I will not start from pangu made the world, a little mention.
First, memory is essential for any application to run.
In addition, the inner existence we mentioned has different meanings on different levels.
๐ป Hardware layer (Hardware)
At the hardware level, memory refers to random-access memory.
Memory is an important part of a computer. It is used to store all kinds of data required by application running. The CPU can directly exchange data with the memory to ensure smooth application running.
Generally speaking, there are two Main types of random-access memory in the composition of a computer: Cache and Main memory.
Caches are usually integrated directly into the CPU, far away from us, so most of the (hardware) memory we refer to is main memory.
๐ก Random Access Memory (RAM)
Random Access Memory is divided into Static Random Access Memory (SRAM) and Dynamic Random Access Memory (DRAM).
SRAM is much faster than DRAM, and SRAM is second only to the registers inside the CPU.
In modern computers, SRAM is used for cache and DRAM is used for main memory.
๐ก Main memory (Main memory)
Although the speed of the cache is very fast, its storage capacity is very small, ranging from a few KB to a few tens of MB, which is not enough to store the data of the application running.
We need a storage unit with moderate storage capacity and speed that allows us to run dozens or even hundreds of applications at the same time while maintaining performance, which is the role of main memory.
The main memory in a computer is actually what we usually call the memory bar (hardware).
Hardware memory is not our topic today, so that’s it. If you want to know more, you can search for the above keywords.
๐งฉ Software layer
At the software level, memory usually refers to the memory space abstracted from main memory by the operating system.
At this point memory can be divided into two types: stack memory and heap memory.
I’m going to talk about memory around JavaScript as a language.
The memory referred to in later articles refers to memory at the software level.
Stack & Heap
Stack memory
๐ก Stack
A stack is a common data structure that allows data to be manipulated at only one end of the structure, all data following the last-in first-out (LIFO) principle.
In real life, the most appropriate example is the badminton bucket, usually we only through the ball bucket side to access, the first to put in the badminton can only be taken out last, and the last to put in will be taken out first.
Stack memory is called stack memory because it uses the stack structure.
Stack memory is a continuous memory space, thanks to the simple and direct stack structure, stack memory access and operation speed is very fast.
Stack memory has a small capacity and is mainly used to store data such as function call information and variables. A large number of memory allocation operations will lead to Stack overflow.
Data storage in stack memory is almost always temporary and is reclaimed as soon as it is used (for example, local variables created in a function are reclaimed when the function returns).
To put it simply: stack memory is suitable for storing data with short life cycle, small footprint and fixed.
๐ก Size of stack memory
Stack memory is managed directly by the operating system, so the size of stack memory is also determined by the operating system.
In general, each Thread has its own stack memory space, and Windows allocates 1MB stack memory for each Thread by default.
Heap memory
๐ก Heap
A heap is also a common data structure, but it is beyond the scope of this article.
Heap memory has the word “heap” in its name, but it has nothing to do with the heap in a data structure.
Heap memory is a large memory space, the allocation of heap memory is dynamic and discontinuous, the program can request heap memory space as needed, but the access speed is much slower than the stack memory.
Data in the heap Memory can exist for a long time, and useless data needs to be recycled actively by the program. If a large amount of useless data occupies the Memory, it will cause a Memory leak.
To put it simply: heap memory is suitable for storing data with a long life cycle and a large or variable footprint.
๐ก Upper limit of heap memory
In Node.js, the default heap memory limit is around 1.4GB on 64-bit systems and 0.7GB on 32-bit systems.
In Chrome, the memory limit for each TAB is about 4 GB (64-bit) and 1 GB (32-bit).
๐ก Process, thread, and heap memory
Typically, a Process has only one heap memory, and multiple threads in the same Process share the same heap memory.
In Chrome, each TAB typically has a separate process, although in some cases multiple tabs can share a process.
Function calling
Now that you understand what stack and heap memory are, let’s look at what happens to stack and heap memory when a function is called.
When a function is called, it is pushed into the Stack memory to generate a Stack frame, which can be understood as a block composed of the return address, parameters and local variables of the function. When a function calls another function, it pushes another function onto the stack, and so on. Until the last function returns, the stack elements are ejected one by one, starting at the top of the stack, until there are no more elements in the stack.
The figure above is simplified, stripping away the concept of stack frames and Pointers to show the general process of function calls and memory allocation.
Under the same thread (JavaScript is single-threaded), all functions being executed, along with their arguments and local variables, are pushed into the same Stack memory, which is why a lot of recursion causes Stack overflow.
See below for more details on the allocation of variables within the function involved.
Store variables
When a JavaScript program is running, local variables generated in a non-global scope are stored in stack memory.
However, only primitive types of variables actually store values in stack memory.
A reference type variable stores only one reference in stack memory, which refers to the actual value in the heap.
๐ก Primitive Type
Primitive types, also known as primitive types, include String, number, BigINT, Boolean, undefined, NULL, and Symbol (new in ES6).
A Primitive value is called a Primitive value.
Note: Even though typeof NULL returns ‘object’, null is not really an object. This is actually a JavaScript Bug
๐ก Reference Type
All types except primitive types are reference types, including Object, Array, Function, Date, RegExp, String, Number, Boolean, and so on…
In fact, Object is the most basic reference type. All other reference types inherit from Object. That is, all values of reference types are actually objects.
A value of a Reference type is called a Reference value.
๐ in a nutshell
In most cases, data of primitive types is stored in stack memory, while data (objects) of reference types are stored in heap memory.
Pay special Attention to
Global variables and variables referenced by closures (even primitive types) are stored in heap memory.
๐ Global variables
All variables created in the global scope become properties of the global object (such as the Window object), that is, global variables.
Global objects are stored in heap memory, so global variables must also be stored in heap memory.
Don’t ask me why global objects are stored in the heap.
๐ฆ Closures
Variables created within a function (local scope) are local variables.
When a local variable is referenced by a function other than the current one (i.e., when an escape occurs), the local variable cannot be reclaimed with the return of the current function and must be stored in heap memory.
And the “other functions” here are what we call closures, as in this example:
function getCounter() {
let count = 0;
function counter() {
return ++count;
}
return counter;
}
// Closure is a closure function
// The variable count escaped
let closure = getCounter();
closure(); / / 1
closure(); / / 2
closure(); / / 3
Copy the code
Closures are a very important and commonly used concept that is found in many programming languages. Here is not a detailed introduction, post a Ruan Yifeng big man’s article.
Learning JavaScript closures:www.ruanyifeng.com/blog/2009/0…
๐ก Escape Analysis
In fact, the JavaScript engine uses escape analysis to determine whether variables should be stored in stack or heap memory.
Simply put, escape analysis is a mechanism for analyzing the scope of variables.
Immutable and Mutable
Stack memory stores two types of variable data: raw values and object references.
Not only are their types different, but they also behave differently in stack memory.
Primitive Values
๐ซ Primitive values are immutable!
As mentioned earlier, raw type data (raw value) is stored directly in stack memory.
(1) When we define a primitive type variable, JavaScript activates a block of memory in the stack to store the value of the variable (the primitive value).
(2) When we change the value of a variable of the original type, we actually activate a new block of memory to store the new value and point the variable to the new memory rather than changing the value in the original block.
(3) When we assign a value from a primitive variable to a new variable (that is, a copy variable), we activate a new block of memory and copy the value from the original variable into the new memory.
๐ค : The original value in stack memory, once determined, cannot be changed (immutable).
Comparison of original values
When we compare variables of primitive type, we directly compare values in stack memory, as long as the values are equal then they are equal.
let a = '123';
let b = '123';
let c = '110';
let d = 123;
console.log(a === b); // true
console.log(a === c); // false
console.log(a === d); // false
Copy the code
Object References
๐งฉ Object references are mutable!
A variable of a reference type is stored in stack memory only as a reference to heap memory.
(1) When we define a variable of reference type, JavaScript first finds a suitable place in the heap memory to store the object, activates a block of stack memory to store the object’s reference (the heap memory address), and points the variable to that block of stack memory.
๐ก So when we access objects through variables, the actual access process should be:
Variables -> references in stack memory -> values in heap memory
(2) When we assign a reference type variable to another variable, we copy the object reference from the stack to which the source variable points to to the stack of the new variable, so we are actually copying the object reference, not creating a new object in the heap.
(3) When we assign a reference variable to a new object, we directly modify the stack reference to which the variable points. The new reference refers to the new object in the heap.
๐ค In short: object references in stack memory can be changed (mutable).
Comparison of objects
All values of reference types are actually objects.
When we compare variables of reference type, we are actually comparing references in stack memory, and the variables are equal only if the references are the same.
Even two reference-type variables that look exactly the same are different as long as their references are not the same value.
// Two variables refer to two different references
// Although the two objects look exactly the same
// But they are really different object instances
let a = { name: 'pp' }
let b = { name: 'pp' }
console.log(a === b); // false
// Direct assignment copies the reference to the object
let c = a;
console.log(a === c); // true
Copy the code
Deep copy of an object
When we understand how reference type variables behave in memory, we can clearly understand why shallow-copy objects are unreliable.
In a shallow copy, a simple assignment simply copies the reference to the object. In fact, both the new variable and the source variable refer to the same object and are modified to the same object, which is obviously not desirable.
To truly copy an object, you must create a new object that copies the properties of the source object. If you encounter an attribute that references the type, create a new object and continue copying…
At this point, we need to use recursion to achieve the replication of multi-level objects, which is also known as deep copy.
For any variable of reference type, use deep copy unless you are sure that the purpose is to copy a reference.
Memory Life Cycle
In general, the memory life cycle of all applications is basically the same:
Allocate -> Use -> release
When we write programs in a high-level language, memory allocation and freeing are often not involved, because allocation and freeing are already implemented in the underlying language.
For JavaScript programs, memory allocation and release are done automatically by JavaScript engines (currently JavaScript engines are mostly written in C++ or C).
But that doesn’t mean we don’t have to worry about memory management. Knowing more details about memory can help you write better and more stable code.
Garbage Collection
Garbage collection is often referred to as THE Garbage collection (GC). It is used to remove data that is no longer needed in memory and free up memory space.
Since stack memory is managed directly by the operating system, when we refer to GC we are referring to garbage collection of heap memory.
Almost all JavaScript engines in modern browsers, such as V8 and SpiderMonkey, implement Garbage collection, and the Garbage collector in the engine periodically collects Garbage.
๐ข urgent remedial lessons
Before we continue, it is important to understand the concepts of “reachability” and “memory leak” :
๐ก Reachability
In JavaScript, reachability refers to whether a variable can be accessed directly or indirectly from a global object. If so, the variable is Reachable, otherwise it is Unreachable.
Both nodes 9 and 10 in the figure above are not directly or indirectly accessible through node 1 (the root node), so they are unreachable and can be safely reclaimed.
๐ก Memory leak
A memory leak is a waste of memory space when a program fails to release unused memory for some reason.
A minor memory leak may not affect your program, but when it becomes serious, it can start to affect your program’s performance and even cause it to crash.
Garbage Collection Algorithms
The basic idea of garbage collection is simple: determine which variable will no longer be used, and then release the memory it occupies.
In fact, it is not easy to determine if a variable is still useful during the recycle process.
While there is no perfect garbage collection algorithm, here are three of the most well-known garbage collection algorithms.
Mark-and-sweep
The tag removal algorithm is one of the most commonly used garbage collection algorithms.
As you can see from the name of the algorithm, the key to the algorithm is marking and clearing.
Tagging refers to the process of marking the state of a variable. There are many ways to mark a variable, but the basic idea is similar.
We don’t need to know all the details of the tagging algorithm, just the basics of tagging.
It should be noted that the efficiency of this algorithm is not high, and it will cause the problem of memory fragmentation.
๐ฐ for example
When a variable enters the execution context, it is marked “in context”; When a variable leaves the execution context, it is marked as “out of context.”
๐ก Execution context
Execution context is a very important concept in JavaScript. Simply put, it is the context in which code is executed.
If you are not familiar with execution context, I highly recommend that you take the time to study it!!
The garbage collector periodically scans all variables in memory, removing markers for variables in and referenced by variables in context, and marking the rest as “to be removed.”
The garbage collector then cleans up any variables marked “to be removed” and frees the memory they occupy.
Mark-compact
To be precise, Compact means Compact, but I think the word “tidy” is more appropriate here.
The tag collation algorithm is also one of the commonly used garbage collection algorithms.
The problem of memory fragmentation can be solved (through collation) by using the tag defragmentation algorithm to improve the availability of memory space.
However, the marking phase of the algorithm is time-consuming and may block the main thread, resulting in the program being unresponsive for a long time.
Although the name of the algorithm is just mark and tidy, the algorithm usually has three phases: mark, tidy and clean.
๐ฐ Take V8’s tag collation algorithm as an example
First, in the tagging phase, the garbage collector will start with the global object (root) and work its way down the hierarchy until all active objects are marked. The remaining untagged objects are unreachable.
โก Then comes the defragmentation phase, where the garbage collector moves live (marked) objects to one end of the memory space, possibly changing the memory address of the objects in memory.
โข At the end of the sweep phase, the garbage collector clears objects behind the boundary (that is, after the last active object) and frees up the memory space they occupy.
Reference Counting (Reference Counting)
The reference counting algorithm is a garbage collection algorithm based on a “reference counting” implementation, which is the most rudimentary but deprecated garbage collection algorithm.
The reference-counting algorithm requires the JavaScript engine to record the number of times each variable is referenced while the program is running, and then determine whether the variable can be recycled based on the number of references.
Although garbage collection no longer uses reference counting algorithms, reference counting techniques are still very useful!
๐ฐ for example
Note: Garbage collection is not valid! But in the following examples we will assume that the collection takes effect immediately, so it will be easier to understand
// I will simply call the object with the name property ฯฯ
// Objects whose name attribute is pp are simply called pp
// The reference to ฯฯ : 1, the reference to pp: 1
let a = {
name: 'PI PI..z: {
name: 'pp'}}// Both b and A refer to ฯฯ
// ฯฯ reference: 2, pp reference: 1
let b = a;
// both x and a.z point to pp
// ฯฯ reference: 2, pp reference: 2
let x = a.z;
// Now only b points to ฯฯ
// ฯฯ references: 1, pp references: 2
a = null;
// Now PI has no references and can be reclaimed
// After PI PI is reclaimed, pp references are reduced accordingly
// The reference to PI is 0, and the reference to pp is 1
b = null;
// Now pp can also be recycled
// The reference to PI is 0, and the reference to pp is 0
x = null;
// Oh no, it's all over!
Copy the code
๐ Circular References
The reference-counting algorithm looks nice, but it has a fatal flaw: it can’t handle circular references.
In the example below, when foo() completes, objects A and B are out of scope and should theoretically be recoverable.
But because they refer to each other, the garbage collector assumes that they are both still being referenced, causing them to never be collected, causing a memory leak.
function foo() {
let a = { o: null };
let b = { o: null };
a.o = b;
b.o = a;
}
foo();
// Even if foo is already executed
// Objects A and B are out of scope
// But a and B are still referring to each other
// Then they will never be recycled
/ / Oops! Memory leak!
Copy the code
Garbage Collection in V8
8 ๏ธ โฃ V8
V8 is an open-source, high-performance JavaScript engine written in C++ by Google.
V8 is one of the most popular JavaScript engines, and it’s used in everything from the Chrome browser to Node.js.
In V8’s memory management mechanism, Heap memory is divided into regions.
Here we focus only on these two areas:
- New Space: Also known as Young Generation, is used to store newly generated objects, which are managed by the Minor GC.
- Old Space: Also known as Old Generation, is used to store objects that survive two GCS, managed by the Major GC.
In other words, as soon as objects in New Space survive two GC’s, they are moved to Old Space and become Old masters.
๐งน do both
V8 implements two garbage collectors internally:
- Minor GC: Also known as a Scavenger, it uses the Cheney’s Algorithm.
- Major GC: Uses the Mark-Compact Algorithm mentioned earlier in this article.
Most of the New objects stored in the New Space are for temporary use, and the capacity of the New Space is small, so Minor GC runs frequently to maintain memory availability.
Objects in Old Space tend to live longer, so Major GC is less frequent, which reduces the performance penalty associated with frequent GC.
๐ฅ Add some magic
As we mentioned in the “tagging Algorithm” above, the tagging process of this algorithm is very time-consuming, so it is easy to cause the application to be unresponsive for a long time.
To improve the user experience, V8 also implements a feature called Incremental marking.
The point of incremental tagging is to break up the tagging work into smaller sections, interspersed with the JavaScript logic of the Main thread, so that it doesn’t block the Main thread for long periods of time.
Of course, incremental tagging comes at a cost. All changes to objects during incremental tagging need to be notified to the garbage collector so that the garbage collector can mark those objects correctly, and “notification” also comes at a cost.
V8 also has Parallel marking and Concurrent marking implemented with Worker threads, which I won’t go into here
๐ค To sum up
In order to improve performance and user experience, V8 has done a lot of internal “manipulation”, this article is only the tip of the iceberg, but enough to make me admire!
All in all, it was Amazing
Memory Management
Or is it: Memory optimization?
Although memory management is not a direct concern when we write code, there are a few things we can do to avoid memory problems and even improve the performance of our code.
Global variables
Global variables can be accessed much faster than local variables, so you should avoid defining unnecessary global variables.
In our actual project development, we will inevitably need to define some global variables, but we must be careful to use global variables.
Because global variables are always reachable, they are never reclaimed.
๐ Remember the concept of “accessibility”?
Because global variables are directly mounted on global objects, that means global variables can always be accessed directly from global objects.
So global variables are always reachable, and reachable variables are never reclaimed.
What should ๐คจ do?
When a global variable is no longer needed, dereference (empty) it so that the garbage collector can free the memory.
// Global variables are not recycled
window.me = {
name: Daniel Wu.speak: function() {
console.log(I was `The ${this.name}`); }};window.me.speak();
// Can be reclaimed only after dereferencing
window.me = null;
Copy the code
Hidden Classes
The actual hidden class is far more complex than this article, but it’s not the main story today, so we’ll leave it at that.
There is a mechanism called “hidden classes” built into V8 to improve the performance of objects.
Every JS object in V8 is associated with a hidden class, which stores information such as the shape of the object (features) and the mapping between property names and properties.
The Memory offset of each attribute is recorded in the hidden class, so that the Memory location of the corresponding attribute can be quickly located in the subsequent access to the attribute, thus improving the access speed of object attributes.
Objects that have exactly the same characteristics (same attributes in the same order) can share the same hidden class when we create them.
๐คฏ Imagine again
We can think of the hidden class as the mold used in industrial production, with the mold, the production efficiency of the product has been greatly improved.
But if we change the shape of the product, then the original mold will not be used, and we need to make a new mold.
๐ฐ for example
In the Devtools Console of Chrome, execute the following code:
/ / object A
let objectA = {
id: 'A'.name: Daniel Wu
};
/ / object B
let objectB = {
id: 'B'.name: 'Eddie Peng'
};
C / / object
let objectC = {
id: 'C'.name: 'Andy Lau'.gender: 'male'
};
// Objects A and B have exactly the same characteristics
// So they can use the same hidden class
// good!
Copy the code
Then take a snapshot of the heap in the Memory panel. The Comparison view in the heap snapshot can quickly find the three objects created above:
Note: How to view objects in memory will be covered in the second part of this article, but for now let’s focus on hidden classes.
In the figure above, it is clear that objects A and B do use the same hidden class.
Object C cannot share a hidden class with the first two objects because it has a gender attribute.
๐ง Dynamically add or delete object properties
Typically, V8 assigns a working hidden class to the object or creates a new hidden class (a new branch) when we change the characteristics of the object dynamically (adding or deleting properties).
For example, to dynamically add a new property to an object:
Note: This operation is called “ready-fire-aim”.
// Add the gender attribute
objectB.gender = 'male';
// The character of object B has changed
// Add a gender attribute that was not there
Object B can no longer share the hidden class with object A
// bad!
Copy the code
Dynamically deleting an object’s attributes results in the same result:
// Delete the name attribute
delete objectB.name;
// A: We are different!
// bad!
Copy the code
However, adding array-indexed Properties doesn’t matter:
The attribute name is an integer, which V8 will handle separately.
// Add 1 attribute
objectB[1] = 'Number group attribute';
// The shared hidden classes are not affected
// so far so good!
Copy the code
๐ Here comes the question
Having said that, hiding classes does seem to improve performance, but what does it have to do with memory?
In fact, hiding classes also takes up memory space, which is a space-for-time mechanism.
If you create a lot of hidden classes and branches due to dynamically adding and deleting object attributes, you end up wasting a lot of memory.
๐ฐ for example
Create 1000 objects with the same properties and you’ll only have 1 more hidden class in memory.
Creating 1000 objects with completely different attribute information creates 1000 more hidden classes in memory.
What should ๐ค do?
Therefore, we should avoid adding and deleting object attributes dynamically. We should declare all necessary attributes in the constructor at once.
If an attribute is no longer needed, we can set the value of the attribute to NULL as follows:
// Empty the age attribute
objectB.age = null;
// still good!
Copy the code
Also, try to declare attributes of the same name in the same order, so that as many objects as possible share the same hidden class.
Even in cases where you cannot share hidden classes, you can at least reduce the number of hidden class branches.
In fact, the performance problem caused by dynamic adding and deleting object attributes is more critical, but due to the limited space of this article, I will not expand on it.
Closure
As mentioned earlier, variables referenced by closures are stored in heap memory.
Let’s focus on memory in closures again, as in the previous example:
function getCounter() {
let count = 0;
function counter() {
return ++count;
}
return counter;
}
// Closure is a closure function
let closure = getCounter();
closure(); / / 1
closure(); / / 2
closure(); / / 3
Copy the code
Now the variable count will not be released as long as we hold the closure.
If you don’t see the risk, let’s imagine that the variable count is not a number, but a huge array. Too many of these closures can be disastrous for memory.
// I call this work: Closure bombs
function closureBomb() {
const handsomeBoys = [];
setInterval(() = > {
for (let i = 0; i < 100; i++) {
handsomeBoys.push(
{ name: 'Tangerine peel'.rank: 0 },
{ name: 'ใไฝ ใ'.rank: 1 },
{ name: Daniel Wu.rank: 2 },
{ name: 'Eddie Peng'.rank: 3 },
{ name: 'Andy Lau'.rank: 4 },
{ name: Aaron Kwok.rank: 5}); }},100);
}
closureBomb();
// Will destroy the world
// ๐ฃ ๐ ๐ฅ ๐จ
Copy the code
What should ๐ค do?
So, we must avoid overusing closures, and be careful with closures!
Remember to dereference closure functions when they are no longer needed, so that closure functions and referenced variables can be reclaimed.
closure = null;
// The variable count is saved
Copy the code
How to Analyze memory
Having said all that, how do we look at and analyze memory when a program is running?
“To do a good job, you must sharpen your tools.”
For Web front-end projects, the best tool for analyzing Memory is Memory!
Memory here refers to a tool in DevTools, but to avoid confusion I’ll use the term “Memory panel” or “Memory panel” below.
๐ง DevTools (developer tools)
DevTools is a set of tools built into the browser for Web development and debugging.
All Chromuim browsers come with DevTools, and I recommend Chrome or Edge (new).
Memory in Devtools
After we switch to the Memory panel, we should see the following screen (note) :
In this panel, we can record memory in three ways:
- Heap snapshot: indicates the Heap snapshot
- Allocation Instrumentation on timeline: Indicates the timeline of memory Allocation
- Allocation Sampling: Memory Allocation sampling
Tip: Click the Collect Garbage button (the trash can icon) in the upper left corner of the panel to actively trigger garbage collection.
๐ค Before we start analyzing memory in earnest, let’s learn a few important concepts:
๐ก Shallow Size
Shallow size refers to the amount of memory currently occupied by the object itself.
The shallow size does not contain objects that are themselves referenced.
๐ก Retained Size
The reserved size refers to the total amount of memory that can be freed after the current object is collected by GC.
In other words, it is the sum of the current size of the object plus the size of other objects that the object refers to directly or indirectly.
Note that the reserved size does not include objects that are directly or indirectly referenced by global objects in addition to being referenced by the current object.
Heap snapshot
Heap snapshots can record the JS objects and THE MEMORY allocation of DOM nodes at the current time of the page.
๐ How to get started
Click the Take Snapshot button at the bottom of the page or the โซ button in the upper left corner to Take a heap snapshot and the results will be displayed automatically after a few moments.
On the heap snapshot results page, we can use four different views to view memory:
- Summary: Summary view
- Comparison: Comparison view
- Containment: Contains views
- Statistics: indicates the statistical view
The Summary view is displayed by default.
Summary (Summary view)
View groups objects according to Constructor. We can filter objects quickly by typing Constructor names in the Class filter.
A few key words in the page:
- Constructor: a Constructor.
- Distance The minimum Distance between the object and the GC root.
- Shallow Size: indicates the Shallow Size (Bytes).
- Retained Size: Retained Size, expressed in Bytes.
- Retainers: holders, variables that refer directly to the target object.
๐ Retainers (Holder)
The Retainers bar is called Object’s Retaining Tree in older Devtools.
The object under Retainers is also expanded into a tree structure, which is convenient for reference tracing.
In the constructor list in the view, there are some entries wrapped with “()” :
- Compiled code: indicates the code already compiled.
- Closure: Closure function.
- (array, string, number, symbol, regexp): Corresponding type (
Array
,String
,Number
,Symbol
,RegExp
). - (concatenated string)Use:
concat()
The concatenated string of functions. - (sliced string)Use:
slice()
,substring()
Functions such as edge cutting of the string. - System: Generated objects of the system, such as the V8 created HiddenClasses and DescriptorArrays data.
๐ก DescriptorArrays
The descriptor array mainly contains the property name information of the object and is an important part of the hidden class.
However, the descriptor array does not contain integer index attributes.
The rest that are not wrapped with “()” are global properties and GC roots.
In addition, each object is followed by a string of numbers beginning with “@”, which is the object’s unique ID in memory.
Tip: Press Ctrl/Command + F to display the search bar. Enter the name or ID to quickly find the target object.
๐ช Practice: instantiate an object
โ Switch to the Console panel and execute the following code to instantiate an object:
function TestClass() {
this.number = 123;
this.string = 'abc';
this.boolean = true;
this.symbol = Symbol('test');
this.undefined = undefined;
this.null = null;
this.object = { name: 'pp' };
this.array = [1.2.3];
this.getSet = {
_value: 0.get value() {
return this._value;
},
set value(v) {
this._value = v; }}; }let testObject = new TestClass();
Copy the code
โก Go back to the Memory panel, create a snapshot of the heap, and type “TestClass” in Class filter:
You can see that there is an instance of TestClass in memory with a shallow size of 80 bytes and a reserved size of 876 bytes.
๐ค Notice that?
The TestClass instance in the heap snapshot is missing an attribute called number because the heap snapshot does not capture numeric attributes.
๐ช Practice: Create a string
โ Switch to the Console panel and execute the following code to create a string:
// This is a global variable
let testString = 'I'm Daniel Wu';
Copy the code
โก Go back to the Memory panel, create a heap snapshot, open the search bar (Ctrl/Command + F) and type “I am Daniel Wu” :
Comparison view
The Comparison option appears only when there are two or more heap snapshots.
The comparison view is used to show the differences between two heap snapshots.
Using a comparison view allows us to quickly see how memory changes (such as adding or reducing objects) after an operation is performed.
Comparing multiple snapshots also allows us to quickly identify and locate memory leaks.
When I mentioned hidden classes earlier in this article, I used the comparison view to quickly find newly created objects.
๐ช Put it into practice
โ Create a new traceless (anonymous) TAB and switch to the Memory panel to create a heap Snapshot Snapshot 1.
๐ก why is the traceless TAB page?
Common tabs are affected by browser extensions or other scripts, and the memory footprint is unstable.
Using tabs with traceless Windows can ensure that the memory of the page is relatively clean and stable, which is conducive to our comparison.
In addition, it is recommended to open the window between sections before starting the test so that memory is stable (control variables).
โก Switch to the Console panel and execute the following code to instantiate an object Foo:
function Foo() {
this.name = 'pp';
this.age = 18;
}
let foo = new Foo();
Copy the code
Select Snapshot 1 as the Base Snapshot and type “Foo” in Class filter.
You can see that an instance of Foo has been added to memory and 52 bytes of memory have been allocated. The reference holder of this instance is the variable Foo.
โฃ Switch to the Console panel again and execute the following code to dereference variable foo:
// Unreference the object
foo = null;
Copy the code
โค Go back to the Memory panel, create a heap Snapshot with Snapshot 3, select Snapshot 2 as the Base Snapshot, and type “Foo” in Class filter:
An instance of the Foo object in memory has been removed, freeing 52 bytes of memory.
Containment view
The inclusion view is the Bird’s eye view of the program object structure, allowing us to explore the details of memory from the global object level by level.
The include view has the following global objects:
GC roots
GC Roots is the actual root node used in garbage collection for the JavaScript virtual machine.
The GC root can be Built in object maps, Symbol tables, VM Thread stacks, Compilation caches, Handle Scopes and Global handles.
DOMWindow Objects
DOMWindow Objects are the top-level objects provided by the host environment (browser), that is, the global object Window in JavaScript code. Each TAB page has its own Window object (even if it is the same window).
Native objects
Native objects are those built-in objects implemented based on the ECMAScript standard, including Object, Function, Array, String, Boolean, Number, Date, RegExp, Math, and so on.
๐ช Put it into practice
Switch to the Console panel and execute the following code to create a constructor $ABC:
Constructors are named with a $in front of them so they can be sorted first.
function $ABC() {
this.name = 'pp';
}
Copy the code
โก Switch to Memory, take a heap snapshot, and switch to Containment:
The constructor we just created, $ABC, can be found under the global object of the current TAB.
Statistics (Statistical view)
The statistical view can visually display the overall memory allocation.
The hollow pie chart in this view has six colors, each meaning being:
- Red: Code
- Green: Strings
- JS Arrays
- Orange: Typed Arrays
- Purple: System Objects
- White: free memory
Allocate the instrumentation on timeline.
Memory allocation is continuously recorded over a period of time (approximately every 50 milliseconds), after which you can choose to view allocation details for any period of time.
You can also check simultaneous Allocation Stacks, which are called stacks, but this incurs an additional performance cost.
๐ How to get started
Click Start button at the bottom of the page or โซ button at the upper left corner to Start recording. During recording, click ๐ด button at the upper left corner to end recording, and the result will be displayed automatically after a moment.
๐ช
โ Open the Memory panel and start recording the allocation timeline.
โก Switch to the Console panel and execute the following code:
Code effect: create 100 objects every 1 second for a total of 1000 objects.
console.log('Test started');
let objects = [];
let handler = setInterval(() = > {
// Create 100 objects per second
for (let i = 0; i < 100; i++) {
const name = `n${objects.length}`;
const value = `v${objects.length}`;
objects.push({ [name]: value});
}
console.log('Number of objects:${objects.length}`);
// Stop after 1000
if (objects.length >= 1000) {
clearInterval(handler);
console.log('End of test'); }},1000);
Copy the code
๐ is another detail
I don’t know if you noticed, but in the code above, I did a bad thing.
When the for loop creates an object, it generates a unique property name and value based on the current length of the object array.
V8 would not be able to optimize these objects for testing.
In addition, there is a surprise in using the length of the object array as the property name
โข Wait quietly for 10 seconds, and the console will print “Test finished”.
โฃ Switch back to the Memory panel and stop recording. After a while, the results page will be automatically displayed.
There are four views for assigning a timeline result page:
- Summary: Summary view
- Containment: Contains views
- Allocation: Allocation view
- Statistics: indicates the statistical view
The Summary view is displayed by default.
Summary (Summary view)
It looks similar to the summary view of the heap snapshot, with a horizontal Timeline at the top of the page.
๐งญ timeline
There are three main lines in the timeline:
- Thin line: scale line for memory allocation
- Blue vertical line: memory is allocated at the corresponding time and is still active at the end
- Gray vertical line: memory is allocated at the corresponding time, but finally reclaimed
A few operations on the timeline:
- Move the mouse pointer to any position in the timeline, click or long press the left button and drag to select a period of time
- Drag the box above the time range to adjust the selected time range
- Move the mouse to the selected time frame and slide the scroll wheel to adjust the time range
- Move the mouse to both sides of the selected time frame and slide the scroll wheel to adjust the time range
- Double-click the left mouse button to deselect
Select a time range in the timeline to view the memory allocation details of the time range.
Containment view
The inclusion view for the allocation timeline is the same as the inclusion view for the heap snapshot, which I won’t repeat here.
Allocation (Allocation view)
Sorry guys, I don’t know what this is for…
Open on the direct error, I: meow meow meow?
Is it because no one uses it that no one knows there’s a problem…
Statistics (Statistical view)
The statistical view of the allocated timeline is the same as the statistical view of the heap snapshot without further elaboration.
Allocation sampling
Introduction on the Memory panel: Use sampling methods to record Memory allocations. This method of analysis has minimal performance overhead and can be used for long-term recording.
Good guy, this brief introduction has enough vague, said with did not say like, very energetic!
I couldn’t find any official documentation on sampling assignment, and Google has almost no information on it. So the following content is only the result of personal practice, if there is any wrong place welcome to point out!
Simply put, by allocating samples we can visually see how much memory is allocated for each function (API) in our code.
Because of the sampling method, the results are not 100% accurate, even though the same operation may have different results each time, but it is enough to give us a general picture of memory allocation.
How โ Starts
Click Start button at the bottom of the page or โซ button at the upper left corner to Start recording. During recording, click ๐ด button at the upper left corner to end recording, and the result will be displayed automatically after a moment.
๐ช
โ Open the Memory panel and start recording and allocating samples.
โก Switch to the Console panel and execute the following code:
The code looks a little long, but it’s just four functions that add objects to the array in different ways.
// Plain single-layer call
let array_a = [];
function aoo1() {
for (let i = 0; i < 10000; i++) {
array_a.push({ a: 'pp' });
}
}
aoo1();
// Two layers of nested calls
let array_b = [];
function boo1() {
function boo2() {
for (let i = 0; i < 20000; i++) {
array_b.push({ b: 'pp' });
}
}
boo2();
}
boo1();
// Three levels of nested calls
let array_c = [];
function coo1() {
function coo2() {
function coo3() {
for (let i = 0; i < 30000; i++) {
array_c.push({ c: 'pp' });
}
}
coo3();
}
coo2();
}
coo1();
// Two layers of nested multiple calls
let array_d = [];
function doo1() {
function doo2_1() {
for (let i = 0; i < 20000; i++) {
array_d.push({ d: 'pp' });
}
}
doo2_1();
function doo2_2() {
for (let i = 0; i < 20000; i++) {
array_d.push({ d: 'pp' });
}
}
doo2_2();
}
doo1();
Copy the code
โข Switch back to the Memory panel and stop recording. After a while, the results page will be displayed automatically.
There are three views available for assigning sample results to the page:
- The Chart view
- Heavy (Bottom Up) : Flat view (call hierarchy Bottom Up)
- Tree (Top Down) : Tree view (call hierarchy Top Down)
I really don’t know how to translate this Heavy, so I named it according to the specific performance.
The Chart view is displayed by default.
The Chart shows that…
The Chart view displays the memory allocation details of each function in a graphical table. You can select the exact memory allocation phases (on the axis of the memory allocation size).
Left-click, drag, and double-click to manipulate the memory allocation phase axis (like the timeline) and select the phase range to view.
Moving the mouse over the function box displays the memory allocation details of the function.
Click the left mouse button function box can jump to the corresponding code.
Heavy (flat view)
The Heavy view flattens the function call hierarchy so that the function is presented as an individual. It is also possible to expand the call hierarchy, but in a bottom-up structure, that is, a reverse function call process.
There are two sizes in the view:
- Self Size: Self Size, which refers to the amount of memory allocated directly within a function.
- Total Size refers to the Total amount of memory allocated by a function, that is, the amount allocated by other functions, including nested calls within the function.
Tree (Tree view)
The Tree view presents the function call hierarchy in a Tree structure. We can start from the source of the code execution, layer by layer, showing a complete forward function call process.
The resources
JavaScript Advanced Programming (Version 4)
The Memory Management:Developer.mozilla.org/en-US/docs/…
Visualizing Memory Management in V8 Engine:Taaz. Tech/memory – mana…
Trash Talk: The Orinoco garbage collector:V8. Dev/blog/trash -…
Fast Properties in V8:V8. Dev/blog/fast – p…
Concurrent marking in V8:V8. Dev/blog/concur…
Chrome DevTools:Developers.google.com/web/tools/c…
portal
Wechat twitter version
Personal blog: Rookie small stack
Open source home page: Chen PI PI
Eazax-ccc game development scaffolding
More share
Why Use TypeScript?
Gaussian Blur Shader
Understanding YAML
Cocos Creator Performance Optimization: DrawCall
Internet Operation Terminology Literacy
“Draw a cool radar in Cocos Creator.”
Write a Perfect Wave with Shader
Gracefully and efficiently managing popovers in Cocos Creator
Cocos Creator source Code interpretation: Engine startup and main Loop
The public,
Novice small stack
๐บ MY name is Pipi Chen, a game developer who is still learning, and a Cocos Star Writer who loves sharing.
๐จ this is my personal public account, focusing on but not limited to game development and front-end technology sharing.
๐ each original is very carefully, your attention is my original power!
Input and output.