This is the fourth day of my participation in the August Text Challenge.More challenges in August

We all know that compiling native code is faster than interpreting it, partly because it saves the extra time the virtual machine spends interpreting bytecode execution. Another is that the virtual machine design team concentrated almost all of their code optimizations into the just-in-time compiler. In this lesson we will take a look at the optimization techniques used by the HotSpot VIRTUAL machine’s just-in-time compiler to compile code, namely escape analysis, scalar replacement, and allocation on the stack.

 

Escape analysis

 

Let’s look at escape analysis in detail first, and then at the two main measures of compiler optimization, scalar replacement and on-stack allocation. You should know that variable replacements and on-stack assignments are done based on escape analysis.

 

Let’s start with escape analysis, which is not a direct way to optimize code, but rather an analysis technique that provides the basis for other optimizations. The basic behavior of escape analysis is analyzing the dynamic scope of an object: when an object is defined in a method, it may be referenced by an external method, such as passing it as a call parameter to another method, called method escape. It can even be accessed by an external thread, such as an assignment to a class variable or an instance variable that can be accessed in another thread, called thread escape. Simply put, escape analysis refers to analyzing whether a variable can escape its scope.

 

If you can prove that an object cannot escape from a method or thread, that is, no other method or thread can access the method in any way, then you can do some efficient optimizations for this variable.

 

Escape analysis can be broken down into four scenarios:

 

First: global variable assignment escape.

Second: method return to escape.

Third: instance reference escape.

Fourth: Thread escape.

 

With that in mind, let’s look at a code example. Create a SomeClass class.

 

public class SomeClass { public void printClassName(EscapeDemo1 escapeDemo1){ System.out.println(escapeDemo1.getClass().getName()); }}Copy the code

 

public static SomeClass someClass; / / global variable assignment escape public void globalVariablePointerEscape () {someClass = new someClass (); }Copy the code

 

GlobalVariablePointerEscape method, we make a copy of a local variable to a static variable, the scope of the local variable is inside the method. The scope of a class variable is inside the class, so the scope is magnified and escape is obvious.

 

Public void someMethod(){SomeClass SomeClass =methodPointerEscape(); } public SomeClass methodPointerEscape(){ return new SomeClass(); }Copy the code

 

With the methodPointerEscape method, we return an object whose scope can also be inside the method initially. But we’re returning it as a return value, so let’s say there’s another method, like someMethod’s method here that calls methodPointerEscape. So someClass is scoped in the someMethod method, so someClass is scoped in the someMethod method in methodPointerEscape. So there was an escape.

 

/ / instance reference escape public void instancePassPointerEscape () {enclosing methodPointerEscape () printClassName (this); }}Copy the code

 

InstancePassPointerEscape method, this passed printClassName method below. The scope of this was originally under the current instance, but now extends under the SomeClass instance, so it also escapes.

 

In addition, we have a thread escape here without doing the code example. Thread escape is easy to summarize. An appearance escape occurs when an assignment is made to a class variable or to an instance variable accessible from another thread.

 

These are the four scenarios in which an object can escape. When the JVM does escape analysis, it will analyze these scenarios. When the analysis is complete, it will mark the object with an escape state. An object has three main escape state flags:

 

First state: global level escape, which indicates that an object may escape from a method or object. That is, other methods or threads can also access the object. There are several scenarios. First, an object is returned as the return value of a method. Second, an object is a static field or a member variable. There are two scenarios that correspond to the global variable assignment escape and method return escape we mentioned earlier. Third, if the Finalize method of a certain class is overridden, the variables of the finalize class will be marked as global escape state and must be placed in the heap memory.

 

Second: parameter level escape. If an object is passed to a method as a parameter, no other method can access the object, and no other thread can access the object. Then it is a parameter level escape, like the strength reference passing we introduced earlier is a parameter level escape.

 

Third: : no escape state, refers to an object does not escape.

 

Scalar replacement

 

Ok, now that we know about escape analysis, let’s talk about scalar substitutions.

 

First of all, what is a scaler? A scaler is something that can’t be further decomposed. Java’s underlying data types (numeric types like int, long, and reference types) and object address references are scalars because they cannot be further decomposed. The opposite of a scalar is an aggregate quantity, which is something that can be further decomposed. For example, a string is an aggregate quantity, because strings are implemented as byte arrays and can be decomposed. For example, the variables defined by ourselves are aggregate quantities.

 

So what is a scalar substitution? Scalar substitution is called restoring the original type of a member variable used by a program to access it, depending on how it is accessed. If escape analysis proves that an object cannot be accessed externally, and that the object can be disassembled, the program may not actually create the object, but instead create its member variants used by the method. In addition to allowing the object’s member variables to be allocated and read and written on the stack (where the data will most likely be allocated by the virtual machine to be stored in high-speed registers on the physical machine), the object can be split up to create conditions for further optimization.

 

For example, suppose you have a Person class. And then inside this class there are some member variables, which are of base types, which are scalars.

 

 public void personTest(){
        Person person=new Person();
        person.id=01;
        person.age=22;
    }


class Person{
     int id; 
     int age;
}
Copy the code

 

If scalar substitution is enabled, the instance of personTest is not created directly, but the personTest member variable is created to replace it. This means that when variable substitution is turned on, the original code is optimized.

 

public void personTest(){ Person person=new Person(); person.id=01; person.age=22; Int id =01; int age=22; }Copy the code

 

So when you make a scalar substitution, you don’t need to allocate memory for the original object. You can use this parameter: -xxeliminateAllocations turns on scalar substitutions, which is enabled by default in JDK 8.

 

So what’s good about scalar substitution? This can greatly reduce the footprint of the heap. Because once you don’t need to create objects, you don’t need to allocate heap memory anymore.

 

On the stack

 

Okay, now let’s talk about on-stack assignments. As we know, the vast majority of objects in the Java virtual machine are stored in the heap, which is almost a common sense for Java programmers. Objects in the Java heap are shared and visible to each thread. As long as you hold a reference to this object, you can access the object data stored in the heap. A virtual machine’s garbage collection system can reclaim objects that are no longer in use from the heap, but the action of collecting is time-consuming, whether it is sifting through recyclable objects or reclaiming and defragmenting memory.

 

However, there is a special case where, after escape analysis, an object is found to have no escape method, then it may be optimized for stack allocation, so that there is no need to allocate memory on the heap and garbage collection is not required.

 

So what is on-stack assignment? This is if escape analysis confirms that the object will not be accessed externally. By allocating objects directly on the stack, the space occupied by the object will be destroyed when the war comes out, so allocating objects on the stack reduces the burden of garbage collection.

 

Synchronization to eliminate

 

Ok, so finally, let’s talk about synchronous elimination. If escape analysis can determine that a variable cannot escape from the thread and be accessed by other threads, then there will be no multi-thread contention for reading and writing the variable, and thus the synchronization of the variable can be eliminated.

 

conclusion

 

Ok, just a quick summary of the lesson. We first saw what escape analysis is. If the variables are not externally accessible after escape analysis, there are two optimizations.

 

One is scalar substitution. Variable substitution can replace the aggregate quantity with several scalar quantities to save memory.

 

The second is stack allocation, which directly allocates the object on the stack, thus reducing the stress of garbage collection.

 

The relevant JVM parameters for this class are as follows:

 

parameter Default (JDK)  
-XX:+DoEscapeAnalysis open Whether to enable escape analysis
-XX:+EliminateAllocations open Whether to enable scalar substitution
-XX:+EliminateLocks open Whether to enable lock cancellation
-XX:+PrintEscapeAnalysis open After escape analysis is enabled, you can view the analysis result by using this parameter.
-XX:+PrintEliminateAllocations  open After scalar substitution is enabled, view scalar substitution.

 

Don’t forget that elimination can be implemented based on escape analysis, so elimination can also be considered an optimization mechanism for just-in-time compilers. About elimination.