preface
According to the Java Virtual Machine Specification, the Class file format uses a pseudo-structure similar to the C-language structure to store data with only two data types: “unsigned numbers” and “tables.”
Bytecode instruction
Java bytecode instructions are the instructions that the Java virtual machine can understand and execute, which can be said to be the assembly language at the Jvm level, or the smallest execution unit of Java code.
The javac command compiles Java source files into bytecode files, known as.class files, which contain a large number of bytecode instructions.
The Java virtual machine uses a stack-oriented rather than registrie-oriented architecture (the execution, differences, and implications of the two architectures are discussed in Chapter 8), so most instructions contain no operands, only one opcode, and the instruction parameters are stored in the operand stack.
JVM tuning practical learning notes address: JVM tuning more than 400 pages of learning notes
Bytecode instruction classification:
Storage and loading instructions: mainly include load series instructions, Store series instructions, LDC and push series instructions, which are mainly used for data scheduling among local variable table, operand stack and constant pool. (There is no special explanation about the constant pool, this is very simple, just like the name implies, this is a pool of various constants, like the set of props.)
Object manipulation directives (create and read/write access) : For example, putField and getfield are read/write access directives. There are putStatic/getStatic directives, new directives, and Instanceof directives.
Operand stack management instructions: such as POP and DUP, which operate only on the operand stack.
Type conversion instructions and arithmetic instructions: add/div/ L2I and so on. In fact, these instructions usually only operate on the operand stack.
Control jump instructions: this class includes the commonly used if series of instructions and goto class instructions.
Method invocation and return directives: Mainly include the Invoke and return series of directives. These instructions also signify the opening and closing of a method space, with invoke invoking a new Universe of Java methods (a new stack and local variable table) and return signaling the closing of the universe.
Public design, private implementation
The VM can be implemented in the following two modes:
· Translate the input Java virtual machine code into another virtual machine’s instruction set at load time or execution time;
· Translate the input Java virtual machine code into the host processor’s native instruction set (i.e., just-in-time compiler code generation technique) at load time or execution time.
The precise definition of virtual machine behavior and object file formats should not limit the creativity of virtual machine implementers. The Java virtual machine is designed to allow many different implementations, and each implementation can provide different new and interesting solutions while maintaining compatibility.
Class file changes
Class file format is platform neutral (independent of specific hardware and operating system), compact, stable and extensible, which is an important pillar of Java technology system to achieve platform independent and language independent characteristics.
Class files are the data entry of the Java virtual machine execution engine and one of the basic pillars of the Java technology architecture.
Vm class loading mechanism
The Java VIRTUAL machine loads the data describing the Class from the Class file to the memory, verifies, transforms, and initializes the data, and finally forms Java types that can be directly used by the VIRTUAL machine. This process is called the virtual machine Class loading mechanism.
Class life cycle
Unlike other languages that require wiring at compile time, the loading, wiring, and initialization of types are all done during program execution. Writing an interface-oriented application can wait until run time to specify its actual implementation classes, and a user can have a native application load a binary stream from the network or elsewhere at run time as part of its program code through Java’s preset or custom classloaders. Runtime loading is widely used in Java programs.
The Java Virtual Machine Specification specifies that there are only six cases in which classes must be “initialized” immediately (and loading, validation, and preparation naturally need to begin before then) :
3) When initializing a class, if the parent class has not been initialized, the initialization of the parent class needs to be triggered first.
4) When the virtual machine starts, the user needs to specify a primary class (the one containing the main() method) to execute, and the virtual machine initializes this primary class first.
The real difference between an interface and a class is the third of the six “have and only” initialization scenarios described earlier: When a class is initialized, all of its parents are required to be initialized. However, when an interface is initialized, its parents are not required to be initialized. The parent interface is initialized only when the parent interface is actually used (for example, referencing constants defined in the interface).
Class loading process
loading
1) Get the binary byte stream that defines a class by its fully qualified name.
2) Convert the static storage structure represented by this byte stream to the runtime data structure of the method area.
3) Generate a java.lang.Class object representing the Class in memory as an access point to the various data of the Class in the method area.
Gets the form of the binary byte stream for the class
· Read from ZIP packages, which was common and eventually became the basis for future JAR, EAR, and WAR formats.
· From the network. The most typical application of this scenario is a Web Applet. · Runtime calculation generation. Dynamic Proxy technology is most commonly used in this scenario. In java.lang.reflection.proxy, Is to use the ProxyGenerator. GenerateProxyClass () for the specific interface generated form of Proxy class for “* $Proxy” binary byte streams.
· Generated by other files, typical scenario is JSP application, JSP file generated by the corresponding Class file. · Read from the database. This scenario is relatively rare, for example, some middleware servers (such as SAP Netweaver) can choose to install programs into the database to complete the distribution of program code between clusters.
· It can be obtained from encrypted files. This is a typical protection against decompilation of Class files. By decrypting Class files at load time, the program running logic is protected from prying eyes.
validation
The verification phase will roughly complete the following four stages of verification: file format verification, metadata verification, bytecode verification and symbol reference verification.
By the Halting Problem, the program accurately checked whether the program ended during its Halting schedule.
To prepare
The phase that formally allocates memory and sets initial values for variables defined in a class (that is, static variables, modified by static).
The first is that only class variables, not instance variables, are allocated in the Java heap along with the object when it is instantiated. The second is that the initial value here is “normally” zero for the data type, assuming that the definition of a class variable is:
public static int value=123;
The initial value of the value variable after the preparation phase is 0 instead of 123, because no Java methods have been executed yet. The putStatic instruction that assigns value to 123 is stored in the class constructor () method after the program is compiled. So assigning value to 123 is not performed until the initialization phase of the class. Table 7-1 lists the zero values for all the basic data types in Java.
parsing
The process by which the Java virtual machine replaces symbolic references in a constant pool with direct references.
Symbolic references are independent of the memory layout implemented by the virtual machine, and the target of the reference is not necessarily something that has been loaded into the virtual machine’s memory. A direct reference is a pointer that can point directly to a target, a relative offset, or a handle that can be indirectly located to the target. Direct references are directly related to the memory layout implemented by the virtual machine
1. Class or interface resolution
You need to determine whether the class is an array type
If we say that a D has access to C, that means that at least one of the following three rules is true:
· The accessed class C is public and in the same module as the accessed class D.
· The accessed class C is public and not in the same module as the accessed class D, but the module accessed class C allows the module accessed class D to access it.
· The accessed class C is not public, but it is in the same package as the accessed class D.
2. Field parsing
The CONSTANT_Class_info symbol reference of the index in the class_index entry in the field table is first resolved, that is, the symbol reference of the class or interface to which the field belongs.
3. Method analysis
First parse out the symbolic reference to the class or interface to which the indexed method belongs in the class_index entry of the method table. If the parse succeeds, then we still use C to represent the class.
1) Since the Class file format is separated from the constant type definition of the method symbol referenced by the Class and interface, if the C index of the class_index is found in the method table of the Class, it is an interface. That is thrown directly Java. Lang. IncompatibleClassChangeError anomalies.
2) If you pass the first step, look in class C to see if there is a method whose simple name and descriptor match the target. If there is, return a direct reference to the method, and the search ends.
3) Otherwise, recursively search the parent class of class C for a method whose simple name and descriptor match the target. If so, return a direct reference to the method, and the search ends.
4) otherwise, the class C list of implementation of the interface and their parent interface of recursive search whether there is a simple name and descriptor with the target matching method, if there is a match, the method of class C is an abstract class, at that time to find the end, throwing Java lang. AbstractMethodError anomalies.
5) otherwise, declaring the method lookup failure, throw out the Java. Lang. NoSuchMethodError. Finally, if the lookup process successfully returned to the reference directly, will the method for authentication, if it is found that do not have access to this method, will throw Java. Lang. IllegalAccessError anomalies.
4. Interface method analysis
Method parsing similar
JDK 9 increased the static private method of the interface, also has the modular access constraints, so since the JDK 9, access interface method is entirely possible for access control and appear Java. Lang. IllegalAccessError anomalies.
Initialize the
The initialization phase is the execution of the class constructor () method.
() method is automatically collected by the compiler in the class all class variable assignment action and static blocks (static {} block) of the statement in merger, the compiler collection order is decided by the order of the statement in the source file, static block can only access to the definition in the static block variables before and after its variables, The previous static block can be assigned, but not accessed.
Unlike class constructors (that is, instance constructor () methods in the virtual machine perspective), the () method does not require an explicit call to the superclass constructor, and the Java virtual machine guarantees that the () method of the superclass is executed before the () method of the subclass is executed. So the first () method to be executed in the Java virtual machine must be of type java.lang.object. · Since the () method of the parent class executes first, it means that the static statement block defined in the parent class takes precedence over the assignment operation of the child class.
Class loader
For any class, the uniqueness of the Java virtual machine must be established by both the classloader that loads it and the class itself. Each classloader has a separate class namespace.
Comparing two classes to be “equal” only makes sense if they are loaded by the same classloader. Otherwise, even if two classes come from the same Class file and are loaded by the same Java virtual machine, as long as they are loaded by different classloaders, the two classes must not be equal.
Equality includes returns from equals(), isAssignableFrom(), isInstance(), and the instanceof keyword.
There are two different class loaders from the Java virtual machine perspective:
Bootstrap ClassLoader C++ implementation
Two: All other class loaders (all inherited from the abstract java.lang.ClassLoader) Java implementation
From a developer’s perspective:
Start the Bootstrap ClassLoader
It is responsible for loading libraries stored in the \lib directory, or in the path specified by the -xbootclasspath parameter, and recognized by the virtual machine (only recognized by filename, such as rt.jar, libraries with incorrect names will not be loaded even in the lib directory) to the virtual machine. The boot class loader cannot be directly referenced by Java programs. If you need to delegate the loading request to the boot class loader when writing a custom class loader, you can use NULL instead.
Extension ClassLoader Extension ClassLoader
The Application ClassLoader
The classloader is implemented by sun.misc.Launcher$AppClassLoader. Since this ClassLoader is the return value of the getSystemClassLoader() method in ClassLoader, it is also commonly referred to as the system ClassLoader. It is responsible for loading libraries specified on the user’s ClassPath and can be used directly by the developer. If the application does not have its own custom class loader, this is generally the default class loader in the application.
Parental delegation model
Parental delegation modelWorkflow:
When a classloader receives a class-loading request, it does not try to load the class itself. Instead, it delegates the request to the parent loader. This is true at every level of classloaders, so all requests should eventually be passed to the starting classloader. Only when the parent class loader reports that it cannot complete the load request (it did not find the desired class in its search scope) will the child loader attempt to load it itself.
Advantages: Java classes have a hierarchical relationship with priority along with their classloaders.
For example: For example, if we want to load java.lang.Object, which is stored in rt.jar, any class loader that loads a different class delegates it to the boot class loader at the top of the model. So the Object class is the same class in every classloader environment of a program (see above for how to compare two classes for ‘equality ‘). On the other hand, if there is no parent delegate model, and each class loader has to load itself, there will be multiple Object classes in the program, resulting in a chaotic application.
The implementation of the parental delegate model
The parent delegate model requires that all class loaders have their own parent class loaders, except for the top-level start class loaders. However, the parent-child relationship between classloaders is not usually implemented in an Inheritance relationship, but usually uses Composition relationships to copy the parent’s code.
Break the parental delegation model
There is no technical way to avoid the possibility that loadClass() may be overwritten by subclasses in order to be compatible with the existing user-defined class loader code. You can only add a new protected method, findClass(), to java.lang.classloader after JDK 1.2 and override it as much as possible when instructing user-written classloading logic instead of writing code in loadClass().
This is caused by a flaw in the model itself, which calls back to the user’s code if there is an underlying type.
As a result of users’ pursuit of dynamic application, the term “dynamic” here refers to some very “Hot” terms: Hot Swap, Hot deployment of modules, etc
Each program module (called a Bundle in OSGi) has its own class loader. When a Bundle needs to be replaced, the Bundle is replaced with the same kind of loader to achieve the hot replacement of code. In the OSGi environment, class loaders move away from the tree structure recommended by the parent delegate model and evolve into a more complex network structure
Java modular System
The Java Platform Module System (JPMS) introduced in JDK 9 is an important upgrade to Java technology. In order to achieve the key goal of modularity, configurable encapsulation and isolation, The Java virtual machine also makes changes to the class loading architecture to enable the modular system to operate smoothly.
· Rules for JAR file access in classpath: All of the classpath JAR files and other resources, are considered automatically packaged in an anonymous Module (Unnamed Module), the anonymous Module is almost without any isolation, it can see all of the packages, and using class path JDK all export package, in the system Module and exported packages of all the modules in the Module path.
· Access rules for modules in the Module path: a Named Module under the Module path can only access the modules and packages listed in its dependency definition. All contents in the anonymous Module are invisible to the Named Module, that is, the Named Module cannot see the contents of traditional JAR packages.
·JAR file access rules in the Module path: If a traditional JAR file that does not contain the Module definition is placed in the Module path, it becomes an Automatic Module. Although not included in module-info.class, automatic modules will depend on all modules in the entire module path by default, thus having access to packages exported by all modules, and automatic modules will export all of their own packages by default.
After JDK9, the Extension ClassLoader is replaced by the Platform ClassLoader.
When class loading platform and application class loader received request * *, * * in front of the delegated to the parent loader loading, to determine whether the class belonging to a certain system module, if this can be found on the ownership of the relations, must be responsible for priority assigned to the module loader finished loading, maybe this is damage to parents delegate for the fourth time.
Virtual machine execution engine
Java virtual machines use methods as the basic execution unit, and Stack frames are the data structures that support method calls and method execution by the VirtualMachine. They are also the Stack elements of the VirtualMachine Stack in the data area when the VirtualMachine runs.
A stack frame stores information about a method’s local variogram, operand stack, dynamic linkage, and method return address.
For each method, the process from invocation to execution corresponds to the process of a stack frame in the virtual machine stack from stack to stack.
Local variable scale
The Local Variables Table is a storage space for a set of variable values. It is used to store method parameters and Local Variables defined within a method. When the Java program is compiled as a Class file, the max_locals data item of the method’s Code property determines the maximum size of the local variable table that the method needs to allocate.
A variable slot can hold a data type up to 32 bits. In Java, there are eight data types that occupy less than 32 bits of storage space: Boolean, byte, CHAR, short, int, float, Reference, and returnAddress.
The seventh reference type represents a reference to an object instance, and any virtual implementation should be able to do at least two things with this reference. One is to find, directly or indirectly, the starting address or index of the object’s data in the Java heap. The other is to directly or indirectly find the type information stored in the method area according to the reference of the data type to which the object belongs. Otherwise, the syntax convention defined in the Java Language Specification cannot be realized.
When a method is called, the Java virtual machine uses the local variable table to pass parameter values to the list of parameter variables, that is, arguments to parameters. If an instance method is executed (a method not modified static), the variable slot in the zeroth index of the local variable table is used by default to pass a reference to the instance of the object to which the method belongs, and the implicit argument can be queried in the method using the keyword “this”.
The operand stack
An Operand Stack, often referred to as an operation Stack, is a Last In First Out (LIFO) Stack. As with the local variable table, the maximum depth of the operand stack is written into the max_stacks data item of the Code attribute at compile time.
The interpreted execution engine of the Java virtual machine is called “stack-based execution engine”, where the “stack” is the operand stack.
Dynamic connection
Each stack frame contains a reference to the method that the stack frame belongs to in the run-time constant pool [inset]. This reference is held to support Dynamic Linking during method invocation.
The Class file has a large number of symbolic references in the constant pool, and the method invocation instructions in the bytecode take symbolic references to methods in the constant pool as arguments. Some of these symbolic references are converted to direct references during class loading or the first time they are used, which is called static resolution. The other part, which is converted to a direct reference at each run, is called the dynamic join.
Method return address
Once a method is executed, there are only two ways to exit the method.
The first way is met an arbitrary execution engine () method returns the bytecode instruction, by this time there may be a return value passed to the upper method caller (called the call method from the caller or tone method), the method has a return value and return value type will be decided according to meet what kind of method to return instructions, This exit Method is called Normal Method InvocationCompletion.
Another exit is when an exception is encountered during the execution of a method that is not handled properly within the method body. This Method exit is called the “Abnormal Method Invocation Completion.” A method exits with an exception completion exit without providing any return value to its upper callers.
After a method exits, it must return to where it was when the original method was called in order for the program to continue. When a method returns, it may need to store some information in the stack frame to help restore the execution state of its upper calling method.
When the method exits normally, the value of the PC counter of the calling method can be used as the return address, and this counter value is likely to be stored in the stack frame. When a method exits with an exception, the return address is determined by the exception handler table, and this information is generally not stored in the stack frame.
Dynamic connections, method return addresses, and other additional information are generally grouped together as stack frame information.
The method call
Method invocation is not equivalent to the code in the method being executed. The only task in the method invocation stage is to determine the version of the method being invoked (that is, which method to call). The specific running process inside the method is not involved for the time being.
parsing
The target method of all method calls is a symbolic reference in a constant pool in the Class file. During the parsing phase of the Class load, the “compile-time knowable, run-time immutable” method symbolic references are converted into direct references. In other words, the call target is determined as soon as the program code is written and the compiler compiles. The invocation of this type of method is called Resolution.
Static methods, private methods, instance constructors, and parent methods, plus methods that are modified by final (even though they are called using the Invokevirtual directive), resolve symbolic references to direct references to the method when the class is loaded. These methods are collectively referred to as “non-virtual methods”, while other methods are referred to as “Virtual methods”.
Dispatch1. Static dispatch
“Method Overload Resolution” is commonly used in English, so it is a dynamic concept
Human hu = new Man():
The “Human” in the code is called Static Type or appearance Type of variables, and the “Man” after it is called Actual Type of variables. Both the Static Type and the Actual Type can change in the program. The difference is that static typing changes only at use, the static type of the variable itself is not changed, and the final static type is known at compile time; The result of the actual type change can only be determined at run time. Compilation time does not know what the actual type of an object is when you compile the program. Right? The following code:
All dispatch actions that rely on static types to determine the version of a method’s execution are called static dispatch. The most typical application of static dispatch is method overloading. ** Static dispatching occurs at compile time, so the action to determine static dispatching is not actually performed by the virtual machine, which is why some sources choose to classify it as “parsing” rather than “dispatching.”
The relationship between analysis and assignment described by the author is not an exclusive relationship between the two, but a process of screening and determining target methods at different levels. For example, as mentioned earlier, static methods are resolved at class load time, and static methods can obviously have overloaded versions, and the selection of overloaded versions is done through static dispatch.
The automatic transformation can continue to occur multiple times, matching char>int> Long >float>double, but will not match byte and short overloads because char to byte or short conversions are unsafe.
Automatic packing
If there are more than one parent class, the search starts from the bottom up in the inheritance relationship. The higher the parent class is, the lower the priority is.
It can be seen that the variable-length argument has the lowest overload priority, when the character ‘a’ is treated as an element of a char[] array.
There are some automatic transitions that are true for a single parameter, such as char to int, that are not true for variable-length parameters
Dynamic dispatch
Another important manifestation of Java language polymorphism is Override.
According to the Java Virtual Machine Specification, the runtime resolution of the Invokevirtual directive [inset] is roughly divided into the following steps:
1) Find the actual type of the object pointed to by the first element at the top of the operand stack, and call it C.
2) If a method is found in type C that matches both the descriptor and the simple name in the constant, the access permission is checked. If the method passes, the direct reference of the method is returned, and the search process ends. Not through the return Java. Lang. IllegalAccessError anomalies.
3) Otherwise, search and verify the second step of each parent class of C from bottom to top according to the inheritance relationship.
4) if you never find the right way, it throws the Java. Lang. AbstractMethodError anomalies.
Because the first step in the invokevirtual directive execution is to determine the actual recipient type at runtime, the Invokevirtual directive in both calls does not end up resolving symbolic references to methods in the constant pool to direct references. Instead, the invokevirtual directive selects method versions based on the actual type of method recipients. This process is the essence of method rewriting in the Java language. We call this dispatch process, which determines the version of method execution at run time based on the actual type, dynamic dispatch.
Since this polymorphism is rooted in the execution logic of the virtual method invocation instruction invokevirtual, it is natural that the conclusion we draw will only be valid for methods, but not for fields, since fields do not use this instruction. In fact, only virtual methods exist in Java. Fields can never be virtual. In other words, fields never participate in polymorphism. ** When a subclass declares a field with the same name as the parent class, the subclass’s field overshadows the parent class’s field, even though both fields exist in the subclass’s memory.
The Father constructor calls showMeTheMoney() as a virtual method. The Father constructor calls showMeTheMoney() as a virtual method. The actual execution of the Son::showMeTheMoney() method, so the output is “I am Son”, after the analysis of the previous readers believe there is no doubt. The Son::showMeTheMoney() method calls the money field of the child class, and the result is still 0 because it will not be initialized until the child class’s constructor executes. The last sentence of main() accesses money in the parent class via static typing, printing 2.
Multiple and single dispatch of methods
The receiver of a method and the parameters of a method are collectively known as method arguments, a definition that probably originated in the famous book Java and Patterns. Dispatches can be divided into single dispatches and multiple dispatches depending on how many cases the dispatches are based on. Single dispatch selects a target method based on one cell, while multiple dispatch selects a target method based on more than one cell.
As a result of this argument, we can conclude that the Java language today (up to Java 12 and the preview version of Java 13 as written in this book) is a statically multi-dispatch, dynamically single-dispatch language.
Dynamic language support
The first new bytecode member released in JDK 7 is the Invokedynamic directive.
A key feature of dynamically typed languages is that the main process of type checking takes place at run time rather than compile time. There are many languages that meet this feature. Common ones include: APL, Clojure, Erlang, Groovy, JavaScript, Lisp, Lua, PHP, Prolog, Python, Ruby, Smalltalk, Tcl, etc. In contrast, languages that do type checking at compile time, such as C++ and Java, are the most commonly used statically typed languages.
Support for dynamically typed languages at the Java virtual machine level has always been lacking, mainly in the area of method calls: The bytecode instruction set prior to JDK 7, Each of the four method invocation instructions ** (Invokevirtual, Invokespecial, Invokestatic, invokeInterface) ** takes its first argument as a symbolic reference to the invoked method (CONSTANT_Methodref_info or CONST) ANT_InterfaceMethodref_info constant), as mentioned earlier, symbolic references to methods are made at compile time, whereas dynamically typed languages can only determine the recipient of a method at run time.
The function of Invokedynamic instruction and MethodHandle mechanism is the same. Both of them aim to solve the problem that the original 4 “Invoke *” instruction method dispatching rules are completely fixed in the VIRTUAL machine, and transfer the decision of how to find the target method from the virtual machine to the specific user code, so that users (generalized users, Designers who include other programming languages) have greater freedom.
Stack – based bytecode interpretation execution engine
Read, understand, and then perform. Before most program code can be translated into object code of a physical machine or an instruction set executed by a virtual machine, the following steps are required:
What’s the difference between a stack-based instruction set and a register-based instruction set? To take the simplest example, using the two instruction sets separately to compute the result of “1+1”, a stack-based instruction set would look like this:
After the two iconst_1 instructions successively push the two constants 1 onto the stack, the iADD instruction pushes the top two values off the stack, adds them together, and then puts the results back on the top of the stack. Finally, istore_0 puts the top value into the 0th variable slot of the local variable table. The instructions in this kind of instruction flow usually take no parameters, using the data in the operand stack as the input of the operation of the instruction, and the operation result of the instruction is also stored in the operand stack.
With a register-based instruction set, the program might look something like this: the MOV instruction sets the value of the EAX register to 1, then the add instruction increases the value by 1, and the result is stored in the EAX register. This two-address instruction is the mainstay of the x86 instruction set, and each instruction contains two separate input parameters that depend on the store.
The main advantage of stack-based instruction sets is portability, because registers are provided directly by the hardware [inset], and programs that rely directly on these hardware registers are inevitably constrained by the hardware.
The main disadvantage of stack instruction sets is that the execution speed is relatively slow in theory. All major physical machine instruction sets are register architectures.
Such as:
Javap suggests that this code needs a stack of operands of depth 2 and a local variable space of four variable slots
Class loading of Tomcat
OSGi (Open Service Gateway Initiative) is a dynamic modularization specification based on Java language developed by OSGi Alliance (JPMS introduced in JDK 9 is a static module system).
Bytecode generation technology and implementation of dynamic proxy
Bytecode generation techniques are used in javac, JSP compilers in Web servers, AOP frameworks woven at compile-time, and dynamic proxy techniques that are commonly used, even when reflection is used it is possible for a virtual machine to generate bytecode at run time to speed up execution.
Dynamic proxy of the “dynamic”, is aimed at using Java code actually writing the proxy class “static” agent, its advantage is not to save the proxy class that coding effort to write, but in the original class and interface also implements the unknown, will determine the agent behavior of the proxy class, when after the proxy class and original class from direct contact, It can be reused flexibly in different application scenarios.
Bridge the gap between JDK versions and deploy code written in older JDK versions into older JDK environments. To solve this problem, a Java Backporting Tools has been created. Retrotranslator[inset] and Retrolambda are among the best of these Tools.
Features added in each update of the JDK can be broadly divided into the following five categories:
2) Improvements made at the front-end compiler level. This improvement, known as syntactic sugar, means that the Javac compiler automatically inserts a lot of integer.valueof (), float.valueof (), and the like in programs where it uses wrapping objects. Variable length parameters are automatically converted to an array after compilation for parameter passing; The generic information is erased at compile time (but remains in the metadata), where the compiler inserts the conversion code automatically [inset].
3) Changes that need to be supported in the bytecode. Dynamic language support, a new syntactic feature in JDK 7, requires an Invokedynamic bytecode instruction to be added to the virtual machine. But the bytecode instruction set remains relatively stable, and such direct changes at the bytecode level are rare.
4) Improvements need to be made to support the overall structure of JDK. A typical example is the Java modular system introduced in JDK 9, which involves JDK structure, Java syntax, class loading and connection process, Java virtual machine and other levels.
5) Focus on improvements within the virtual machine. Such as the Java Memory Model (JMM) redefined by the JSR-133 specification implemented in JDK 5, and the G1, ZGC, and Shenandoah collectors added in JDK 7, JDK 11, and JDK 12, This change is basically transparent to programmers writing code and only affects the program while it is running.
The concept of compilation
The process by which a *.java file is converted into a *.class file by the front-end compiler (or, more accurately, “the front end of the compiler”);
The process by which the Java virtual machine’s just-in-time Compiler (often called the JIT Compiler, Just In Time Compiler) converts bytecode to native machine code at runtime;
Refers to using a static AOT Compiler (often called AOT Compiler, Ahead Of Time Compiler).
The optimization process of real-time compiler in Java supports the continuous improvement of program execution efficiency. The optimization process of front-end compiler during the compilation period supports the improvement of programmer’s coding efficiency and language user’s happiness.
Compile – one preparation for three processes
1) Preparation: Initialize the plug-in annotation handler.
2) Process of parsing and filling symbol table, including: · Lexical and grammatical analysis. An abstract syntax tree is constructed by converting the character stream of the source code into a tag set. · Populate the symbol table. Generates symbolic addresses and symbolic information.
3) Annotation processing of the plug-in annotation processor: During the execution phase of the plug-in annotation processor, the actual section of this chapter designs an plug-in annotation processor to influence the compilation behavior of the Javac.
4) Analysis and bytecode generation process, including: · Annotation check. Check for static information about the syntax. · Data flow and control flow analysis. Check the dynamic running process of the program. · Solution sugar. Return syntactic sugar that simplifies code writing to its original form. · Bytecode generation. Convert the information generated by the previous steps into bytecode.
When inserting annotations, new symbols may be generated, and if any new symbols are generated, they must be reprocessed by going back to the previous parsing and filling of the symbol table
The plug-in annotation processor is thought of as a set of plug-ins for the compiler 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.
Semantic analysis and bytecode generation
1. Check labels
The annotation check step checks things like whether variables are declared before they are used and whether the data types between variables and assignments match.
Constant Folding code optimization: Defining ‘a=1+2’ in code does not increase the amount of processing done in a single processor clock cycle than defining ‘A =3’ in code.
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, and whether all checked exceptions are handled correctly.
Syntax sugar generics
The nature of generics is the use of Parameterized Type, or ParametricPolymorphism, which specifies the data Type of an operation as a special parameter in a method signature that can be used in the creation of classes, interfaces, and methods. Constitute generic classes, generic interfaces, and generic methods, respectively.
Java’s chosen implementation of Generics is called Type Erasure Generics, while C#’s is Reified Generics.
Generics in the Java language is different, it only exists in the program source code, in the compiled bytecode file, all generic is substituted for the original bare Type (Raw Type, later we will explain what is naked Type concrete), and the code in the appropriate place to insert force transformation, thus for the Java language runtime, ArrayList and ArrayList are actually the same type
In the non-generics era, since arrays in Java are Covariant supported, there are options for introducing generics:
1) The types that need to be generalized (mainly container types) are left unchanged, and then a new set of generic versions are added in parallel.
2) Generalize existing types directly, that is, make all existing types that need to be generized in situ, without adding any generic versions that are parallel to existing types.
Let’s continue with ArrayList as an example of how type erasure is implemented for Java generics. Because Java takes the second route, it generics existing types directly. To make all existing types that need to be genericized, such as ArrayList, become ArrayLists, and to make sure that the code that used ArrayList directly will continue to use the same container in the new version of the generics, all instance types that need to be genericized, For example, ArrayList and ArrayList all automatically become subtypes of ArrayList, otherwise the conversion is not safe. This leads to the concept of a “Raw Type,” which should be considered the common Super Type of all generic instances of that Type.
How to implement bare typing. There are two options: one is for the Java virtual machine to automatically and realistically construct a type like ArrayList at runtime and automatically implement an inheritance relationship derived from ArrayList to satisfy the definition of bare type; The other option is to simply revert an ArrayList back to an ArrayList at compile time, automatically inserting casts and checkers only when elements are accessed or modified.
Generics implemented in this way are called pseudo-generics.
This code is compiled into a Class file and then decompiled with a bytecode decompiler to convert the generic types back to native types
Disadvantages of the Java generic erase implementation:
Support for Primitive Types is a new problem. If you can’t convert primitypes, you should support primitypes. You all use ArrayList, ArrayList, and all of the primitypes are cast automatically. This decision later led to countless costs of constructing wrapper classes and boxing and unboxing them, which became a major reason for the slowness of Java generics and one of the issues that the Valhalla project is addressing today.
2. The generic type information cannot be obtained during runtime.
Since List and List are of the same type when erased, we can only add two return values that are not actually used to complete the overload.
In addition, from the appearance of the Signature attribute, we can also conclude that the so-called erasure is only the bytecode in the Code attribute of the method. In fact, the metadata still retains the generic information, which is the basic basis for us to obtain the parameterized type through reflection during coding.
Conditional compilation
Define a final variable that separates the code in the if statement.
Because the compiler optimizes the code, the Java compiler will not generate bytecode for statements where the condition is always false.
Application scenario: implement a program to distinguish DEBUG and RELEASE mode.
Covariant and contravariant
Invert and covariant are used to describe the inheritance relationship after Type transformation. Definition: IF A and B represent type, F (⋅) represents type transformation, and ≤ represents inheritance (for example, A≤B indicates that A is A subclass derived from B).
F (⋅) is contravariant, and f(B)≤f(A) is valid when A≤B;
F (⋅) is covariant, and f(A)≤f(B) is valid when A≤B;
F (⋅) is invariant. When A≤B, neither of the above two formulas holds, i.e., f(A) and F (B) have no inheritance relationship with each other.
Arrays are covariant
Generics are immutable
Generics use wildcards for covariant and contravariant implementations. PECS: producer-extends, consumer-super.
List
List
You can assign an appleList to the foodList, but you cannot add any object to the foodList other than null.
The method takes covariant parameters and returns contravariant values:
Through discussion with netizen iamzhoug37, updated below.
Call method result = method(n); According to the Liskov substitution principle, the typeof the passed parameter n should be a subtype of the method parameter, i.e. Typeof (n)≤typeof(method’s parameter); Typeof (Methods’ return)≤typeof(result)
The back-end compiler
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.
Efficient concurrent
At constant prices, the number of components that can be accommodated on an integrated circuit doubles roughly every 18-24 months, and so does its performance.
CPU has been increasing exponentially for a long time, but in recent years, the CPU frequency has remained at about 4G Hertz, which cannot be further improved. Moore’s Law is failing.
Number of processors parallel ratio
The computing speed of a computer is too different from the speed of its storage and communication subsystems, and too much time is spent on disk I/O, network communication, or database access.
Transactions PerSecond (TPS) is one of the important indicators to measure the performance of a service. It represents the total number of requests that the server can respond to on average in a second, and TPS value is closely related to the concurrency capability of the program
Java memory model Main memory and working memory
The main purpose of the Java memory model is to define access rules for various variables in a program, focusing on the low-level details of storing variable values into and out of memory in the virtual machine. Variables include instance fields, static fields, and elements that make up array objects, but not local Variables and method parameters, which are thread private and cannot be shared. For better performance, the Java memory model does not limit the use of execution engines
Specific registers or caches of the processor are used to interact with main memory, and there is no constraint on whether the just-in-time compiler can perform optimizations such as adjusting the order in which code is executed.
The Java Memory model specifies that all variables are stored in Main Memory (which is the same name as the Main Memory mentioned in the introduction of physical hardware, but is physically only a part of the virtual machine’s Memory). Each thread also has its own Working Memory, which contains the main Memory copy of variables used by the thread. All operations on variables (reading, assigning, etc.) must be performed in Working Memory. It cannot read or write data directly from main memory [inset]. Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed through the main memory.
Implementation details about the protocol of interaction between main memory and working memory, i.e. how a variable is copied from main memory to working memory and synchronized from working memory back to main memory.
· Lock: A variable applied to main memory that identifies a variable as a thread-exclusive state.
· UNLOCK: A variable that operates on main memory. It releases a locked variable so that it can be locked by another thread.
· Read: a variable acting on main memory that transfers the value of a variable from main memory to the thread’s working memory for subsequent load action.
· Load: Variable applied to working memory, which puts the value of the variable obtained by the read operation from main memory into a copy of the variable in working memory. · Use: variable applied to working memory, which passes the value of a variable in working memory to the execution engine. This operation will be performed whenever the virtual machine reaches a bytecode instruction that needs to use the value of the variable.
· Assign: a working memory variable that assigns a value received from the execution engine to the working memory variable. This operation is performed whenever the virtual machine accesses a bytecode instruction that assigns a value to the variable.
· Store: Variable applied to working memory that transfers the value of a variable in working memory to main memory for subsequent write operations.
· Write: a variable operating on main memory, which puts the value of the variable obtained from the working memory by the store operation into the main memory variable.
If you want to copy a variable from main memory to working memory, read and load are performed sequentially. If you want to synchronize variables from working memory back to main memory, store and write are performed sequentially. Note that the Java memory model only requires that these two operations be performed sequentially, but not consecutively. This means that other instructions can be inserted between read and load, and between store and write. For example, when accessing variables A and B in main memory, one possible order is readA, read B, Load B, and load A. In addition, the Java memory model also stipulated in the above 8 kinds of basic operation must meet the following rules: not allowed to read and the load, store, and one of the write operation alone, that does not allow a variable from the main memory read but not to accept the working memory, or working memory back wrote but main memory not to accept. · A thread is not allowed to discard its most recent assign operation, that is, a variable that has changed in working memory must be synchronized back to main memory. · A thread is not allowed to synchronize data from the thread’s working memory back to the main memory without a reason (without any assign operation).
Volatile will have two properties. The first is to ensure that the variable is visible to all threads. In this case, “visibility” means that when one thread changes the value of the variable, the new value is immediately known to other threads. This is not the case with ordinary variables, whose values are passed from thread to thread through main memory. For example, if thread A modifies the value of A common variable and writes back to main memory, thread B will read back to main memory after thread A has written back, and the value of the new variable will be visible to thread B.
The second semantics is to prohibit instruction reordering optimization. Ordinary variables can only ensure that correct results can be obtained in all places that depend on assignment results during the execution of this method, but cannot guarantee that the order of variable assignment operations is consistent with the order of execution in the program code.
The definition of special rules for volatile variable definitions in the Java memory model. Given that T represents a thread and V and W represent volatile variables, read, Load, use, assign, Store, and write operations must comply with the following rules: · Thread T can perform use action on variable V only if the previous action performed by thread T on variable V is load; Also, thread T can load variable V only if the next action performed by thread T on variable V is use. The use action of thread T on variable V can be considered to be associated with the load and read actions of thread T on variable V and must occur consecutively and together.
Atomicity
Access, read and write to basic data types are atomic (the exceptions are the non-atomic protocols of long and double)
Visibility
Normal variables differ from volatile variables in that the special rules of volatile ensure that new values are immediately synchronized to main memory and flushed from main memory immediately before each use
There are two other Java keywords for visibility: synchronized and final. The visibility of synchronized blocks is obtained by the rule that a variable must be synchronized back to main memory (store, write) before unlock is performed. Visibility of the final keyword means: Once a field modified by final is initialized in the constructor and the constructor does not pass a reference to “this” (this reference escape is a dangerous thing because other threads may access the “half-initialized” object through this reference), the value of the final field is visible in other threads.
Orderliness
The natural orderliness of Java programs can be summed up in the following sentence: if viewed within the thread, all operations are ordered; If you observe another thread in one thread, all operations are out of order. In this paper, we introduce the concept of “within-threadas-if-serial Semantics” and the concept of “reordering Semantics” and “synchronization delay between working memory and main memory”. In this paper, we introduce the concept of “within-threadas-if-serial Semantics” and “reordering Semantics”.
Principle of antecedent
Some “natural” antecedents under the Java memory model exist without the assistance of any synchronizer and can be used directly in coding. If the relationship between two operations is not in this column and cannot be deduced from the following rules, they are not guaranteed order and the virtual machine can reorder them at will.
· Monitor Lock Rule: In a thread, actions written earlier take place before those written later, in order of control flow. Note that we are talking about the control flow sequence, not the program code sequence, because we have branches, loops, and so on to consider.
· Monitor Lock Rule: an UNLOCK operation occurs first when a subsequent Lock operation is performed on the same Lock. What must be emphasized here is “the same lock”, and “behind” refers to the sequence of time.
· Volatile Variable Rule: Writes to a volatile Variable occur first and then reads occur later, again in chronological order.
· Thread Start Rule: The Start () method of the Thread object occurs first for each action of the Thread.
· Thread Termination Rule: All operations in a Thread occur before the Thread terminates. We can check whether the Thread:: Join () method ends or whether the Thread::isAlive() method returns the value.
· Thread Interruption Rule: Interrupt () calls to the interrupt() method when Thread::interrupted() code detects that the Interruption has occurred.
· Finalizer Rule: The finalization of an object (the end of constructor execution) takes place at the beginning of its Finalize () method first.
· Transitivity: If operation A precedes operation B and operation B precedes operation C, it follows that operation A precedes operation C.
There is basically no causal relationship between the time order and the antecedent principle, so we should not be disturbed by the time order when we measure the concurrency safety problem, and everything must be subject to the antecedent principle.
Threads Three implementations of threads
Implementation using kernel threads (1: 1 implementation) — Kernel-level Thread (KLT) is directly supported by the operating system Kernel (Kernel), a high-level interface of Kernel Thread — LightWeight Process (LWP), Lightweight processes are what we normally call threads.
The cost of system call is relatively high, and it needs to switch back and forth between User Mode and Kernel Mode. Second, each lightweight process needs to be supported by a kernel thread, so lightweight processes consume certain kernel resources (such as kernel thread stack space), so the number of lightweight processes supported by a system is limited.
Use User Thread implementation (1: N implementation) – a Thread that is not a kernel Thread can be considered a User Thread (UT)
Use a hybrid implementation of user threads and lightweight processes (N: M implementation).
User threads are still built entirely in user space, so user threads are still cheap to create, switch, and destruct, and can support large-scale user thread concurrency. The lightweight process supported by the operating system acts as a bridge between the user thread and the kernel thread, so that the thread scheduling function and processor mapping provided by the kernel can be used, and the system call of the user thread is completed through the lightweight process, which greatly reduces the risk of the entire process being completely blocked.
Mainstream Java virtual machines are kernel threaded implementations
Thread Scheduling refers to the process in which the system assigns processor rights to Threads. There are two main ways of Scheduling, namely Cooperative threads-scheduling and Preemptive threads-scheduling.
Collaborative scheduling – if the use of collaborative scheduling multi-threaded system, the execution time of the thread by the thread itself to control, the thread after the completion of their own work, to take the initiative to inform the system to switch to another thread.
The Java language sets 10 levels of Thread priority (thread.min_priority through thread.max_priority). The Windows system has seven thread priorities
Java-defined thread state:
The six states are:
· New: Threads that have not been started after creation are in this state.
· Runnable: includes Running and Ready in the operating system thread state, that is, the thread in this state may be executing or waiting for the operating system to allocate time for it to execute.
· Waiting indefinitely: Threads in this state are not allocated processor execution time; they wait to be explicitly woken up by another thread. ■ There is no Object::wait() method with Timeout; ■ Thread::join() without Timeout; S LockSupport: : park () method.
· Timed Waiting: Threads in this state are also not assigned processor execution time, but instead of Waiting to be explicitly woken up by other threads, they are automatically woken up by the system after a certain amount of time. ■Thread::sleep(); ■ Object:: Wait () with Timeout; ■ Thread::join() with Timeout set S LockSupport: : parkNanos () method; S LockSupport: : parkUntil () method.
· Blocked: A thread is Blocked. The difference between the Blocked state and the wait state is that the Blocked state is waiting to acquire an exclusive lock. This event occurs when another thread abandons the lock. A “wait state” is waiting for a certain amount of time, or wakeup action, to occur. The thread enters this state while the program is waiting to enter the synchronization zone.
· Terminated: The state of a Terminated thread. The thread is Terminated.
coroutines
Java’s current concurrent programming mechanism is somewhat at odds with the above architectural trends. The 1:1 kernel thread model is the mainstream choice for Java virtual machine threads implementation today, but the natural disadvantages of this mapping to the operating system are high switching costs, scheduling costs, and limited number of threads the system can accommodate.
The cost of kernel thread scheduling mainly comes from the state transition between user state and kernel state, while the cost of these two state transitions mainly comes from the cost of responding to interrupt, protecting and resuming execution site.
So what coroutines do is, instead of using threads for a blocked business operation, we use coroutines, so that when you have AN IO block, and you’re not done with the timeslice, you don’t let the CPU run away, you just call up your other coroutine and let it continue. In general, we know that code pure computation execution is very fast, 5ms May run N methods, so this makes full use of the time slice, and reduces the CPU switching time.
Thread safety
When multiple threads access to an object at the same time, if don’t have to consider these threads in the runtime environment of scheduling and execution alternately, also do not need to undertake additional synchronization, or any other coordinated operation in the caller, call the object’s behavior can get the right results, it is said that an object is thread-safe.
Data shared by various operations in the Java language can be classified into the following five categories in order of “safety” of thread safety:
immutable
In addition to String, enumeration types and some subclasses of java.lang.Number are commonly used, such as numeric wrapper types like Long and Double, and big data types like BigInteger and BigDecimal. But AtomicInteger and AtomicLong, both subtypes of Number, are mutable
Absolute thread safety
Relative thread safety
Relative thread safety is what we generally speaking thread-safe, it need to make sure that the object of a single operation is thread-safe, we do not need additional when calling to safeguard measures, but for some particular sequence of consecutive calls, may need to end the call to use additional synchronization method to ensure the correctness of the call.
The thread is compatible with
Thread opposite
An example of thread-antagonism is the suspend() and resume() methods of the Thread class. If you have two threads simultaneously holding a thread object, one trying to interrupt and the other trying to resume, the target thread is at risk of deadlock regardless of whether the call is synchronized or not — if suspend() is interrupted by the same thread that is about to resume(), Deadlocks are bound to occur. It is for this reason that both the suspend() and resume() methods have been declared obsolete.
Thread-safe implementation
1. Mutually exclusive synchronization
It is one of the most common and primary concurrency correctness guarantees. Also known as Blocking Synchronization.
Synchronization refers to ensuring that shared data is used by only one (or a few, when using semaphores) thread at a time when multiple threads concurrently access the data. Mutex is a means to achieve synchronization. CriticalSection, Mutex and Semaphore are common ways to achieve Mutex. Therefore, in the word “mutually exclusive synchronization”, mutual exclusion is the cause, synchronization is the effect; Mutual exclusion is the method, synchronization is the destination.
In Java, the most basic means of mutually exclusive synchronization is the synchronized keyword, which is a BlockStructured synchronization syntax. The synchronized keyword is compiled by Javac and forms two bytecode instructions, Monitorenter and Monitorexit, respectively, before and after the synchronized block. Both bytecode instructions require a reference parameter to specify which object to lock and unlock.
· Synchronized synchronized blocks are reentrant to the same thread. This means that the same thread can enter the synchronized block repeatedly without locking itself.
· A synchronized block unconditionally blocks the entry of subsequent threads until the thread holding the lock completes execution and releases the lock. This means that the thread that acquired the lock cannot be forced to release it, as is done with locks in some databases; There is also no way to force a thread waiting on a lock to interrupt the wait or timeout out.
ReentrantLock is also reentrant and functionally a superset of synchronized: wait interruptible, fair locks can be implemented, and locks can be bound to multiple conditions.
Synchronized releases locks automatically, and locks need to be released manually in finally.
2. Non-blocking synchronization
The optimistic concurrency strategy based on collision detection, in popular terms, is to do the operation first regardless of the risk, if there are no other threads competing for the shared data, then the operation succeeds directly. If the shared data is indeed contended for and a conflict occurs, other compensations are performed, the most common of which is to retry until there is an uncontended shared data. Implementation of this optimistic concurrency strategy eliminates the need to block and suspend threads, so such Synchronization operations are called non-blocking Synchronization, and code that uses this measure is often referred to as lock-free programming.
· Compare and Swap (CAS)
If A variable V is A value when it is first read and is still A value when it is about to be assigned, does that mean that its value has not been changed by another thread? This is not possible, because if its value has been changed to B and later changed back to A during this period, the CAS operation will assume that it has never been changed. This vulnerability is called the “ABA problem” of CAS operations.
The solution is to use version numbers
3. No synchronization scheme
Synchronization is only a means to ensure the correctness of shared data contention. If a method does not involve shared data in the first place, it does not need any synchronization measures to ensure its correctness
Reentrant code
This Code, also known as Pure Code, can be interrupted at any point in its execution to execute another piece of Code (including the recursive call itself) without any errors or consequences for the original program after control is returned.
Thread local storage
The java.lang.ThreadLocal class implements thread-local storage. In each Thread Thread objects have a ThreadLocalMap object, the object to store a set of ThreadLocal. ThreadLocalHashCode as the key, to a Thread local variable as the value of K – V value pairs, A ThreadLocal object is an access point to the current thread’s ThreadLocalMap. Each ThreadLocal object contains a unique threadLocalHashCode value that can be used to retrieve the corresponding local thread variable in the thread k-V pair.
Lock the optimization
The spin lock
If the physical machine has more than one processor or processor core that allows two or more threads to execute in parallel at the same time, we can tell the next thread that requests the lock to “wait a minute” without giving up the processor’s execution time to see if the thread that holds the lock will release the lock soon. To make a thread wait, we simply have the thread perform a busy loop (spin), a technique known as spin locking.
There must be a limit, and if the number of spins exceeds the limit and the lock is not successfully acquired, the thread should be suspended the traditional way.
JDK6 introduces adaptive spin. Adaptive means that the spin time is no longer fixed, but determined by the previous spin time on the same lock and the state of the lock owner. If the spin wait has just successfully acquired a lock on the same lock object, and the thread holding the lock is running, the virtual machine will assume that the spin wait is likely to succeed again, allowing the spin wait to last a relatively long time, such as 100 busy cycles. On the other hand, if the spin rarely succeeds in acquiring the lock for a particular lock, it is possible to simply omit the spin process in future attempts to acquire the lock to avoid wasting processor resources.
Lock elimination
Lock elimination refers to the fact that the virtual machine just-in-time compiler, while running, requires some code to be synchronized, but it detects that there is no possibility of a shared data contention lock elimination. The primary determination of lock elimination is based on data support from escape analysis. If it is determined that in a piece of code, all data on the heap will not escape and be accessed by other threads, it can be treated as data on the stack. It is considered to be thread private, and synchronization locking is no longer necessary.
Lock coarsening
If a series of consecutive operations repeatedly lock and unlock the same object, even if the locking operation occurs in the body of the loop, frequent mutex synchronization can lead to unnecessary performance losses, even if there is no thread contention. If the virtual machine detects a string of fragmented operations that lock the same object, the scope of lock synchronization will be extended (coarsened) outside the entire operation sequence.
Lightweight lock
“Lightweight” is as opposed to traditional locks implemented using operating system mutexes, so traditional locking mechanisms are called “heavyweight” locks.
The HotSpot VIRTUAL machine Object Header
Before the code enters the synchronization block, if the synchronization object flag bit is 01 and is not locked -> then a space named Lock Record is established in the stack frame of the current thread, which is used to store the copy of the current MarkWord of the Lock object.
The virtual machine will use the CAS operation to try to update the object’s Mark Word to a pointer to the Lock Record. If the update action succeeds, it means that the thread owns the lock on the object, and the object’s Mark Word lock bit (the last two bits of the Mark Word) changes to “00”, indicating that the object is in a lightweight locked state. If the update fails, it means that at least one thread is competing with the current thread to acquire the lock on the object.
The virtual machine first checks whether the Mark Word of the object refers to the stack frame of the current thread. If it does, the current thread already owns the lock of the object. If not, the lock object has been preempted by another thread. If there are more than two threads competing for the same lock, the lightweight lock is no longer valid and must be expanded to the heavyweight lock. The status of the lock flag changes to “10”, at which point the pointer to the heavyweight lock (mutex) is stored in the Mark Word, and the thread waiting for the lock must also enter the blocking state.
The unlock process is also carried out by CAS operation. If the object’s Mark Word still points to the lock record of the thread, the CAS operation will replace the object’s current Mark Word and the copied product Mark Word in the thread back. If the replacement is successful, the synchronization process is complete. If the replacement fails, another thread has attempted to acquire the lock, and the suspended thread must be awakened at the same time the lock is released.
The JVM is a virtual machine
Biased locking
If a lightweight lock uses a CAS operation to eliminate the mutex used in synchronization without contention, a biased lock eliminates the entire synchronization without contention, even the CAS operation.
The lock is biased in favor of the first thread that acquired it, and if the lock is never acquired by another thread during subsequent execution, the thread that holds the biased lock will never need to synchronize again.
Assume that biased locking is enabled on the current VM (enable -xx: +UseBiased Locking, which is the default of the HotSpot VIRTUAL machine since JDK 6), when the lock object is first picked up by a thread, the VIRTUAL machine will set the flag bit in the object header to ’01’ and the bias mode to ‘1’ to indicate that the lock object is in bias mode. The CAS operation is also used to record the ID of the thread that acquired the lock in the object’s Mark Word. If the CAS operation succeeds, the VM does not perform any synchronization operations (such as locking, unlocking, and updating the Mark Word) every time the thread that holds the biased lock enters the lock related synchronization block.
After a consistent hash code has been computed, an object can no longer enter the biased lock state; When an object is currently in a biased lock state and receives a request to compute its consistent hash code [inset], its biased state is immediately revoked and the lock expands to a heavyweight lock. In the implementation of heavyweight lock, the object header points to the position of the heavyweight lock. The ObjectMonitor class representing the heavyweight lock has a field that can record the Mark Word under the unlocked state (flag bit is “01”), which can naturally store the original hash code.