JavaScript Execution Mechanism (6) How V8 executes JavaScript code
This week, I bought another course by Mr. Li Bing on Geek Time, a course on the introduction to the principles of Google V8. Many knowledge points, including the overall execution process of V8, how variables are stored, how objects can quickly retrieve properties, how functions are compiled, how closures are implemented, how different calling methods affect the distribution of stacks, etc.
I feel that there is a lot of content, so I will continue the original series. This time, I will summarize my understanding of the process of executing JavaScript in V8 after reading it.
The processor cannot directly recognize code written in high-level languages. What should we do? Usually, there are two ways to execute these codes.
The first is to interpret the execution. You need to compile the input source code into intermediate code through the parser, and then directly use the interpreter to interpret and execute the intermediate code, and then directly output the result. This requires the interpreter to simulate the implementation of CPU and memory by itself. Some features, such as heap and stack.
The second is compile execution. When using this method, we also need to convert the source code into intermediate code first, and then our compiler compiles the intermediate code into machine code. Usually, the compiled machine code is stored in binary file form, and when you need to execute this program, you can directly execute the binary file. You can also use a virtual machine to save the compiled machine code in memory, and then directly execute the binary code in memory.
So, as a kind of JavaScript virtual machine, how does V8 execute JavaScript code? Is it interpreted execution or compiled execution?
In fact, V8 does not use a single technology, but a mixture of compile execution and interpreted execution. We call this mixed use of compiler and interpreter technology JIT (Just In Time) technology.
This is a trade-off strategy because both methods have their own advantages and disadvantages. Explanation execution starts fast but executes slowly, while compile execution starts slow but executes fast. You can refer to the following complete flowchart of V8 executing JavaScript:
Initialization environment
Let’s look at the leftmost part in the above figure first. Before V8 starts executing JavaScript, it also needs to prepare some basic environments needed to execute JavaScript. These basic environments include “heap space”, stack space “, global execution context”, global scope “, message loop system”, “built-in function”, etc. These contents are all needed in the process of executing JavaScript, such as:
JavaScript global execution context contains global information in the execution process, such as some built-in functions, global variables and other information;
The global scope contains some global variables, and the data during execution needs to be stored in memory;
V8 uses the classic heap and stack memory management mode, so V8 also needs to initialize the heap and stack structure in memory;
In addition, for our V8 system to come alive, we also need to initialize the message loop system. The message loop system includes message drivers and message queues. It is like the heart of V8, constantly accepting messages and deciding how to process them.
Generate AST
After the basic environment is ready, you can submit the JavaScript code to be executed to V8.
First, V8 will receive the JavaScript source code to be executed, but this is just a bunch of strings for V8. V8 cannot directly understand the meaning of this string. It needs to structure this string.
Structured means that information can be decomposed into multiple interrelated components after analysis, and there is a clear hierarchy between the components, which is convenient to use and maintain, and has certain operating specifications.
After the V8 source code is structured, a Syntax Tree (AST) is generated, which we call AST. AST is a structure that is easy for V8 to understand.
Regarding how to generate AST, you can take a look at my other article aboutBabel的系列博客, talked about how Babel parses a string of code into an AST. Although it is not exactly the same as V8, the parsing idea is the same
Generate bytecode
With AST and scope, the next step is to generate bytecode, which is intermediate code between AST and machine code. However, regardless of the specific type of machine code, the interpreter can directly interpret and execute the bytecode, or compile it into binary machine code through the compiler and then execute it.
It should also be noted here that while generating AST, V8 will also generate related scopes, which store related variables. There are two more key points here. First,作用域链I have already talked about this in my previous blog, and there is how closure is achieved, which is actually very interesting. I will continue to summarize in the next blog.
Interpretation of the implementation
Well, after the bytecode is generated, the interpreter appears, it will interpret the execution bytecode in order and output the execution result.
I believe you have noticed that we drew a monitoring robot near the interpreter, which is a module that monitors the execution state of the interpreter. In the process of interpreting and executing bytecode, if a certain piece of code is found to be executed repeatedly, then The monitoring robot will mark this code as hot code.
When a piece of code is marked as hot code, V8 will throw this bytecode to the optimization compiler, which will compile the bytecode into binary code in the background, and then perform optimization on the compiled binary code. Operation, the execution efficiency of the optimized binary machine code will be greatly improved. If this code is executed next, V8 will prioritize the optimized binary code, so that the execution speed of the code will be greatly improved.
However, unlike static languages, JavaScript is a very flexible dynamic language. The structure and properties of objects can be arbitrarily modified at runtime, and the optimized code of the optimized compiler can only be used for a certain fixed structure., Once the structure of the object is dynamically modified during execution, the optimized code is bound to become invalid code. At this time, the optimized compiler needs to perform anti-optimization operations. After the anti-optimized code, it will be executed next time. Return to the interpreter to interpret and execute.