preface
Learning resources from Geek Time – Teacher Li Bing “Browser working principle and Practice”. Next, let’s check in every day
- Day 01 Chrome architecture: Why 4 processes with only 1 page open?
- Day 02 TCP: How to ensure that a page file can be delivered to the browser in its entirety?
- Day 03 HTTP Request Flow: Why do many sites open quickly the second time?
- Day 04 Navigation flow: What happens between entering the URL and presenting the page?
- Day 05 Rendering Flow: How do HTML, CSS, and JavaScript become pages?
- JavaScript execution mechanisms in the Day 06 browser
- Day 07 V8 how it works
Content forecast
What will you learn from reading this article?
- Stack space vs. heap space: How is data stored?
- Garbage Collection: How is garbage data automatically collected?
- Compilers and interpreters: How does V8 execute a piece of JavaScript code?
JavaScript’s memory mechanism
Remember the following concepts:
1. JavaScript isWeakly typed, dynamic language.
-
Static languages are those that require validation of their variable data types before use.
-
Languages that need to check data types during runtime are called dynamic languages.
-
JavaScript is a dynamic language because you don’t need to confirm the data type of a variable before declaring it.
Languages that support implicit typing are called weakly typed, and languages that do not are called strongly typed.
What do these characteristics mean?
-
Weakly typed, which means you don’t have to tell the JavaScript engine what data type this or that variable is; the JavaScript engine will figure it out as it runs the code.
-
Dynamic, which means you can use the same variable to hold different types of data. Dynamic, which means you can use the same variable to hold different types of data.
2. Three types of JavaScript memory space:Code space, stack space, heap space
- Code space: The primary storage of executable code
- Stack space: Holds data values of primitive types
- Heap space: Holds values of reference types
Why is there a “heap” and a “stack” storage space?
Because the JavaScript engine needs to use the stack to maintain the state of the context during the execution of the program, if the stack space is too large, all the data will be stored in the stack space, which will affect the efficiency of the context switch, and thus affect the execution efficiency of the entire program.
Therefore, in general, the stack space is not set too large, mainly used to store some primitive type of small data. Reference data takes up a lot of space, so this kind of data will be stored in the heap, the heap space is large, can store a lot of large data, but the disadvantage is that allocating memory and reclaim memory will take a certain amount of time
3. Assignment in JavaScript
- The assignment of the original type copies the value of the variable
- The assignment of a reference type is a copy of the reference address
Talk about the closure
function foo() {
var myName = "Geek Time"
let test1 = 1
const test2 = 2
var innerBar = {
setName:function(newName){
myName = newName
},
getName:function(){
console.log(test1)
return myName
}
}
return innerBar
}
var bar = foo()
bar.setName(Geek Bang)
bar.getName()
console.log(bar.getName())
Copy the code
When executing this code, there is an analysis like this:
Since the variables myName, test1, and test2 are primitive, they are pushed onto the call stack when foo is executed. When foo completes execution, the execution context of foo on the call stack is destroyed, along with its internal variables myName, test1, and test2.
However, when the execution context of function foo is destroyed, the variables myName and test1 are not destroyed, but are stored in memory, because the foo function generates a closure.
- When the JavaScript engine executes on foo, it first compiles and creates an empty execution context.
- When the internal function setName is encountered during compilation, the JavaScript engine also performs a quick lexical scan of the internal function and finds that the internal function references the variable myName in foo. Since the internal function references the variable of the external function, the JavaScript engine determines that this is a closure. Create a “Closure (foo)” object in the heap space (this is an internal object that JavaScript cannot access) to hold the myName variable.
- Look up variables are in a process, I’m not sure yet, because the variable address through the scope chain is needed to find the right address, but the address of the variable of the closure is certain, not need to find the scope, closure directly to the address written on the execution of code, it is because of that address references, wouldn’t the closure at the end of the destruction. So in memory model terms, a closure is a block of address in the heap that can only be held by the referenced function. From a language perspective, closures are heap memory + reference functions, which makes sense. After all, no one can access the memory except reference functions.
- The JavaScript engine then adds test1 to the “Closure (foo)” object when it continues to scan into the getName method and finds that the function also references the variable test1. The “closure(foo)” object in the heap now contains the myName and test1 variables.
- Because test2 is not referenced by an internal function, test2 remains in the call stack.
From the above analysis, we can draw the call stack state when executing the “return innerBar” statement in foo, as shown below:
You can clearly see from the figure above that when foo is executed, the closure is generated; When foo is finished executing, the returned getName and setName methods both refer to an object called “clourse(foo),” so even if foo exits, “Clname (foo)” is still referenced by its internal getName and setName methods. So the next time you call bar.setname or bar.getName, you create an execution context that includes “clourse(foo).”
In summary, there are two core steps to generating closures: the first step is to pre-scan the internal functions; The second step is to save the external variables referenced by the inner function to the heap.
The garbage collection
Stack garbage collection:
When the function completes execution, the JS engine destroys the execution context stored in the stack (variable, lexical, this, outer) by moving the ESP pointer (the pointer that records the current execution state of the call stack).
Heap recycling:
-
Intergenerational hypothesis
- Most objects are short-lived
- Objects that are not destroyed live longer
-
classification
In V8, the heap is divided into two regions: Cenozoic generation and old generation. Objects with short survival time are stored in the Cenozoic generation, and objects with long survival time are stored in the old generation.
- The new generation
Algorithm: Scavenge.
Principle:
- 1) The Cenozoic space is divided into two regions, one is the object region and the other is the idle region.
- 2) The newly added objects are stored in the object area. When the object area is nearly full, a garbage cleaning operation needs to be performed.
- 3) First mark the garbage in the object area, and then copy the surviving objects to the free area.
- 4) After the replication is completed, the roles of the object area and the free area are reversed, that is, the original object area becomes the free area, and the original free area becomes the object area.
Object promotion strategy: Objects that survive two garbage collections are moved to the old area.
- Familiar with
Algorithm: Mark-sweep algorithm:
Principle:
- 1) Marking: The marking stage is to start from a group of root elements and recursively traverse this group of root elements. In this traversal process, the elements that can be reached are called active objects, and the elements that cannot be reached can be judged as junk data.
- 2) Clear: remove garbage data.
Pieces:
Multiple execution of the mark-clear algorithm on a piece of memory results in a large number of discrete memory fragments. Too much fragmentation causes large objects to fail to allocate enough contiguous memory.
Algorithm: Mark-Compact algorithm:
Principle:
- 1) Tagging: Like the tagging process of mark-clean, we start with a set of root elements and recursively traverse the set of root elements. In this traversal process, the reachable elements are marked as active objects.
- 2) Decluttering: Move all surviving objects to one end of memory
- 3) Clear: clear the memory beyond the end boundary
Incremental Marking algorithm principle:
- 1) In order to reduce the old generation of garbage recycling caused by the lag
- 2) V8 breaks a complete garbage collection task into many smaller tasks
- 3) Alternate garbage collection tags with JavaScript application logic
The pause
V8 handles garbage collection using both the secondary garbage collector and the main garbage collector, but since JavaScript runs on the main thread, once the garbage collection algorithm is executed, it is necessary to pause the executing JavaScript script and resume the script execution after garbage collection is complete. We call this behavior stop-the-world.
Compiler, interpreter
Generate abstract syntax tree (AST) and execution context
Some applications of AST:
- Code converter Babel
- ESLint
How is AST generated?
The first stage is tokenize, also known as lexical analysis
- Token refers to the smallest single character or string that is syntactically impossible to divide
The second stage, parsing, is also called parsing
- Convert the token data generated in the previous step to the AST based on the syntax rules
Generate bytecode
Bytecode: CODE that is intermediate between AST and machine code. It needs to be converted to machine code by the interpreter before it can be executed.
Execute the code
Bytecode execution for the first time: the interpreter Ignition explains execution point by point.
What the interpreter does:
- Generate bytecode
- Explains the execution of bytecode
To explain the execution of bytecode:
- Spotting HotSpot code (such as a piece of code that has been repeatedly executed), TurboFan, a compiler in the background, compiles that HotSpot’s bytecode into efficient machine code
- When the optimized code is executed again, only the compiled machine code is executed, which greatly improves the efficiency of the code.
In V8, JIT means the interpreter Ignition collects code information while interpreting and executing the bytecode, and when it sees a section of code getting hot, the TurboFan compiler pops up, converts the hot bytecode into machine code and saves it. For later use.
The following figure shows the JIT process:
JavaScript performance optimization
To optimize JavaScript execution efficiency, you should focus on the execution time per script and the network download of the script:
- Improve the execution speed of a single script, avoid the long task of JavaScript to occupy the main thread, so that the page can quickly respond to interaction;
- Avoid large inline scripts, because parsing and compiling also occupy the main thread while parsing HTML;
- Reduce the size of JavaScript files, as smaller files increase download speed and use less memory.