From the first days of computer programming, the pursuit of efficiency has been a programmer’s innate belief, a never-ending Formula One race in which the programmer is the driver and the technology platform is the racing car.

Front-end compilation and optimization

Front-end compilation refers to the process of converting.java files into.class files, such as the Javac compiler in the JDK

Javac compiler

It is a program written by Java language, and the code is stored in tools.jar. From the overall structure of Javac code, the compilation process can be roughly divided into one preparation process and three processing processes

Preparation: Initialize the plug-in annotation handler.

Processing process:

  1. The process of parsing and populating symbol tables.
  2. Annotation processing by the plug-in annotation processor
  3. Analysis and bytecode generation process

In each of the above three processes, new symbols may be generated when performing the plug-in annotation. If any new symbols are generated, the new symbols must be reprocessed by going back to the previous process of parsing and filling the symbol table

Javac compile action entrance is com. Sun. View Javac. Main. JavaCompiler class

1. Parse and populate the symbol table

The analysis process includes two steps of lexical analysis and syntax analysis in classical program compilation principle

1. Lexical and grammatical analysis

Lexical analysis is the process of converting the character flow of source code into a collection of tokens. Individual characters are the smallest elements at program writing, but tokens are the smallest elements at compile time. Keywords, variable names, literals, and operators can all be used as markers. For example, the code “int A =b+2” contains six markers, namely int, a, =, b, +, and 2. Although the keyword int consists of three characters, it is only an independent marker and can not be split again. In the source of Javac, lexical analysis process by com. Sun. View Javac. Parser. The Scanner class.

Syntax analysis is the process of constructing Abstract Syntax Tree (AST) based on tag sequences. AST is a Tree representation used to describe the Syntax structure of program code. Each node of the abstract Syntax tree represents a Syntax Construct in the program code. Packages, types, modifiers, operators, interfaces, return values, and even code comments can all be a specific Syntax Construct.

Once the syntax tree is generated through lexical and syntax analysis, the compiler does not operate on the source stream of characters, and all subsequent operations are based on the abstract syntax tree.

JDT AST View is an abstract syntax tree for the current code, as shown in the figure below

2. Fill in the symbol table

After completing the parsing and lexical analysis, the next stage is the process of filling the symbol table.

Symbol Table is a data structure composed of a group of Symbol addresses and Symbol information. Readers can imagine it as the storage form of key-value pairs in hash Table by analogy (in fact, Symbol Table is not necessarily hash Table, but can be ordered Symbol Table, tree Symbol Table, stack structure Symbol Table and other forms). The information registered in the symbol table is used at different stages of compilation. For example, in the process of semantic analysis, the content registered in symbol table will be used for semantic check (such as checking whether the use of a name is consistent with the original declaration) and generate intermediate code. In the generation stage of object code, symbol table is the direct basis for address allocation when the symbol name is allocated.

2. Annotation processing of plug-in annotation processor

The plug-in annotation processor can be thought of as a set of compiler plug-ins that, when working, allow arbitrary elements in the abstract syntax tree to be read, modified, and added. If these plug-ins make changes to the syntax tree during annotation processing, the compiler will go back to parsing and populating the symbol table and reprocess it until no more modifications have been made to the syntax tree by any plug-in annotation processor, called a Round.

Java’s famous coding efficiency tool Lombok, for example, relies on plug-in annotation handlers to automatically generate getter/setter methods, perform vacancy checks, generate tables of checked exceptions, and generate equals() and hashCode() methods through annotations.

3. Semantic analysis and bytecode generation

After parsing, the compiler gets an abstract syntax tree representation of the program code. The abstract syntax tree can represent a source program with correct structure, but it cannot guarantee that the semantics of the source program are logical. The main task of semantic analysis is to check the context-related properties of structurally correct source programs, such as type checking, control flow checking, data flow checking, and so on.

int a = 1; boolean b = false; Char c = 2 int d = a + c; int d = b + c; char d = a + c;Copy the code

In the above coded IDEA, we can see the error prompts marked by red lines, most of which are from the inspection results in the semantic analysis stage.

In the compilation process of Javac, semantic analysis can be divided into two steps: annotation check and data and control flow analysis

1. Check labels

The annotation checking step checks things like whether variables are declared before they are used, whether the data types between variables and assignments match, and so on. The three examples of variable definitions just fall within the scope of annotation checking. As part of the annotation check, a code optimization called Constant Folding occurs, which is one of the few optimizations that the Javac compiler does to source code (code optimizations are almost always done in the just-in-time compiler)

2. Data and control flow analysis

Data flow analysis and control flow analysis are further validation of program context logic, which can check whether local variables are assigned before use, whether each path of a method has a return value, whether all checked exceptions are handled correctly, and so on. The purpose of data and control flow analysis at compile time is basically the same as that at class load time, but the scope of verification is different. Some verification items can only be performed at compile time or run time.

3. Bytecode generation

In the bytecode generation stage, not only the information generated in the previous steps (syntax tree, symbol table) is converted into bytecode instructions and written to disk, but also a small amount of code addition and conversion is done by the compiler.

After traversing and tweaking the syntax tree, the bytecode is printed to generate the final Class file, and the compilation process is complete.

Java syntax sugar

In general, using syntactic sugar reduces the amount of code, increases the readability of the program, and thus reduces the chance of errors in the program code.

The most common syntax sugars in Java include generics, varied-length arguments, auto-boxing and unboxing, and so on. They are not supported directly by the Java virtual machine. They are restored to the original base syntax structure at compile time, a process known as decoding sugar.

1. The generic

The nature of generics is the application of Parameterized Type or Parametric Polymorphism.

The Java Generics implementation is called Type Erasure Generics.

Map<String, String> map = new HashMap<String, String>(); Map. Put ("hello", "hello"); System.out.println(map.get("hello")); ----------- Map Map = new HashMap(); ----------- Map = new HashMap(); Map. Put ("hello", "hello"); System.out.println((String) map.get("hello")));Copy the code

The so-called erasing method only erases the bytecode in the Code attribute of the method. In fact, the metadata still retains generic information, which is the fundamental basis for us to obtain parameterized types through reflection during coding.

2. Automatic packing, unpacking and traversing cycle

List<Integer> list = Arrays.asList(1, 2); int sum = 0; for (int i : list) { sum += i; } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the compiled -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the List List = Arrays. The asList (new Integer [] { Integer.valueOf(1), Integer.valueOf(2) }); int sum = 0; for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) { int i = ((Integer)localIterator.next()).intValue(); sum += i; }Copy the code

Auto-boxing and unboxing are translated after compilation into the corresponding wrapper and restore methods, the integer.valueof () and integer.intValue () methods above, while traversal loops restore code to an implementation of iterators. This is why the Iterable loop requires that the class being iterated implement the Iterable interface.

Back-end compilation and optimization

When bytecode is considered an Intermediate Representation (IR) of a programming language, the compiler converts Class files into binary machine code relevant to the local infrastructure (hardware instruction sets, operating systems) whenever and in whatever state. It can be seen as the back end of the whole compilation process.

Just-in-time compiler

The virtual machine interprets a method or block of Code as “Hot Spot Code” when it detects that it is being run particularly frequently. To improve the efficiency of Hot Spot Code execution, the virtual machine interprets the Code into native machine Code at runtime. The back-end compiler that performs this task at run time is called a just-in-time compiler.

  • Interpreters and compilers

The HotSpot VIRTUAL machine contains both an interpreter and a compiler. Once the application is started, the compiler comes into play over time and compiles more and more code into local code, thus reducing the interpreter’s intermediate wastage and achieving higher execution efficiency. When the program is running in an environment where memory resources are limited, you can use interpreted execution to save memory (for example, in some embedded systems and most JavaCard applications, only the interpreter exists), whereas you can use compiled execution to improve efficiency. As a result, the interpreter and compiler often work in tandem throughout the Java Virtual Machine execution architecture.

Because just-in-time compilers take time to compile native code, it usually takes longer to compile optimized code; In order to compile more optimized code, the interpreter may have to collect performance monitoring information for the compiler, which also affects the speed at which the execution phase can be interpreted. In order to achieve the optimal balance between program startup response speed and operation efficiency,

The HotSpot VIRTUAL machine adds hierarchical compilation capabilities to the compilation subsystem:

  • 0 layer. Programs execute purely interpretively, and the interpreter does not turn on Profiling.
  • Level 1. Use the client compiler to compile bytecode into native code to run, for simple and reliable stable optimization, without enabling performance monitoring.
  • Layer 2. The client compiler is still used for execution, with limited performance monitoring functions such as method and loopback statistics enabled.
  • Layer 3. The client compiler is still used, all performance monitoring is enabled, and all statistics such as branch hops, virtual method invocation versions are collected in addition to layer 2 statistics.
  • Layer 4. Bytecode is compiled into native code using a server-side compiler, which executes more compile-time-consuming optimizations than a client-side compiler, as well as aggressive and unreliable optimizations based on performance monitoring information.

Implementation of layered compiled, compiler and interpreter, client server compiler will work at the same time, the hot code compilation, may be more time with the client compilers to obtain a higher speed, with the service side compilers in order to get better quality, at the time of interpretation and without additional, undertake the task of collecting performance monitoring information, When the server compiler uses a high-complexity optimization algorithm, the client compiler can first use simple optimization to gain more compilation time.

  • Compile objects and trigger conditions

The just-in-time compiler builds for “hot code,” which falls into two main categories:

  • A method that is called multiple times.
  • The body of a loop that is executed multiple times.

The more a method is called, the more times the code inside the method is executed, so it is natural that it becomes a “hot code”. The latter is to solve the problem that when a method is called only once or a small number of times, but the method body is stored in the loop body with more times, then the code in the loop body is also repeated many times, so the code should also be considered as “hot code”.

In the first case, since the compilation is triggered by a method call, the compiler naturally takes the entire method as the compilation object, which is standard just-in-time compilation in virtual machines. For the latter, while compiling movement is triggered by the loop body, hot spots are a part of methods, but the compiler still must compile the overall approach as object, just perform entrance (starting from the method which the bytecode instruction of line) will be slightly different, compile bytecode into execution entry points serial number (Byte Code Index, BCI). Because compilation takes place while the method is executing, it is vividly referred to as “On Stack Replacement” (OSR), where the method is replaced while the Stack frame of the method is still On the Stack.

To know if a piece of Code is Hot and needs to trigger an on-the-fly compilation, this is called Hot Spot Code Detection.

Counter based HotSpot detection is used in the HotSpot virtual machine, where each method (even a block of code) has a counter that counts the number of times the method is executed, and if it is executed more than a certain threshold it is considered a “HotSpot method”.

HotSpot has two types of counters for each method: the Method Invocation Counter and the Back Edge Counter (” Back Edge “means jump Back at the loop boundary).

A method call counter counts the number of times a method is called over a period of time. When a certain time limit is exceeded, a method’s call Counter is reduced by half if it hasn’t been called enough times to commit to the just-in-time compiler, a process called Counter Decay. This period is called the Counter Half Life Time counted by this method. The heat attenuation is carried out during garbage collection by the VIRTUAL machine. You can use the vm parameter -xx: -usecounterdecay turns off heat decay, allowing the method counter to count the absolute number of method calls so that most methods in a program will be compiled into local code if the system runs long enough.

Method call counters trigger just-in-time compilation

The loopback counter is used to count the number of times the loop body code is executed in a method. In bytecode, the instruction that controls the redirection is called “Back Edge”. Obviously, the purpose of setting up the loopback counter statistics is to trigger the substitution compilation on the stack.

When the interpreter in a back edge instruction, will first lookup will execute the code snippet to whether have already compiled version, if any, it will give priority to execute the compiled code, or add a back side to the value of the counter, and then judge method invocation counter and counter value, is more than the sum of back to back side counter threshold. When the threshold is exceeded, an on-stack replacement compilation request is submitted and the value of the backside counter is lowered slightly to continue the loop in the interpreter, waiting for the compiler to print the compilation results

The backside counter triggers just-in-time compilation

  • The build process

By default, the virtual machine continues to execute code interpreted until the compiler has finished compiling, whether it is a standard compilation request from a method call or a stack replacement compilation request, while the compilation takes place in a background compilation thread. The user can use the -xx: -backgroundcompilation parameter to disable BackgroundCompilation. If BackgroundCompilation is disabled, the execution thread submits a compilation request to the vm and waits until the compilation process is complete before executing the local code output by the compiler.

A compiler such as Javac that converts Java code to bytecode is called a “front-end compiler” because it does nothing more than generate abstract syntax trees or intermediate bytecode from the program. A “back-end compiler” inside the Java VIRTUAL machine does the code optimization and native machine code generation from bytecode, known as a just-in-time compiler. The speed and quality of the backend compiler is one of the most important indicators of Java virtual machine performance.

reference

Understanding the Java Virtual Machine in depth: Advanced JVM features and Best Practices (3rd edition)