Technology Sharing

Popular Science Article: Understand JVM Practice in One Article (Part 4) In-depth Understanding of Escape Analysis

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Overview

Are all objects in Java allocated in heap memory?

Ok, that’s too abstract. Let’s be more specific and see where the memory is allocated for the following object.

  1. public void test() {
  2. Object object = new Object();
  3. }
  4. 这个方法中的object对象,是在堆中分配内存么?

As a result, the object may allocate memory on the stack or on the heap.

Here comes the point: In the implementation of JVM, in order to improve the performance of JVM and save memory space, JVM provides a feature called "escape analysis". Escape analysis is a cutting-edge optimization technology in the current Java virtual machine and a very important optimization technology in JIT. JDK6 began to introduce this technology, JDK7 began to enable escape analysis by default, JDK8 began to improve escape analysis and enable it by default, and until JDK 9, escape analysis will be used as the default optimization method and no special compilation parameters are required.

Now we understand the sentence "object may be allocated on the stack or on the heap". Before JDK7, the object here must be allocated on the heap; JDK7 and 8 may be allocated on the stack, because JDK7 began to support escape analysis; JDK9 is likely to be allocated on the stack (the object here is very small), because JDK9 truly supports and enables escape analysis by default.

With the development of JIT compilers (Just-in-Time Compilers) and the gradual maturity of escape analysis technology, stack allocation and scalar replacement optimization technology will make "all objects will be allocated on the heap" less absolute. In the Java virtual machine, objects are allocated memory on the heap, but there is a special case, that is, if it is found after escape analysis that an object has not escaped from the method, then it may be optimized to be allocated on the stack. When the method is executed, the stack frame is popped and the object is released. In this way, there is no need to allocate memory on the heap and go through garbage collection (Hotspot does not currently do this). This is also the most common off-heap storage technology.

After JDK 6u23 (remember the major version JDK7), escape analysis is enabled by default in Hotspot. If you use an earlier version, you can enable escape analysis explicitly through the option "-XX:+DoEscapeAnalysis". You can view the filter results of escape analysis through "-XX:+PrintEscapeAnalysis".

Hotspot implements scalar replacement through escape analysis (non-escaped objects are replaced with scalars and aggregates, which can improve code efficiency), but non-escaped objects will still allocate memory on the heap, so it can still be said that all objects are allocated memory on the heap.

In addition, based on the deep customization of Open JDKTaoBao VMThe innovative GCIH (GC invisible heap) technology implements off-heap, moving objects with a long life cycle from the heap to outside the heap, and the GC does not manage the Java objects inside the GCIH, thereby reducing the GC collection frequency and improving the GC collection efficiency.

Stack:When each method is executed, a stack frame is created to store local variable table, operation stack, dynamic link, method exit and other information. The process of each method being called until the execution is completed corresponds to the process of a stack frame being pushed into and popped out of the virtual machine stack.

heap:When an object is instantiated, it is allocated on the heap and a reference to the heap is pushed onto the stack.

escape:When a pointer to an object is referenced by multiple methods or threads, we say that the pointer has escaped. Generally, return objects and references to global variables will also escape.

Escape analysis:The method used to analyze this escape phenomenon is called escape analysis

Escape analysis optimization-allocation on the stack:Allocation on the stack means that the instances generated by local variables in the method (no escape occurs) are allocated on the stack, without being allocated in the heap. After the allocation is completed, execution continues in the call stack. Finally, the thread ends, the stack space is reclaimed, and the local variable objects are also reclaimed.

Java object memory allocation process

  1. If stack allocation is enabled (escape analysis), the JVM will first perform stack allocation.
  2. If stack allocation is not enabled or does not meet the conditions, TLAB allocation will be performed.
  3. If TLAB allocation is unsuccessful or not compliant, determine whether to enter the old generation allocation
  4. If you cannot enter the old generation, enter the eden allocation
  5. Not all objects are allocated on the heap. In addition to the heap, objects can also be allocated to the stack and TLAB. (Most objects are allocated on the heap)

Are objects in Java always allocated on the heap?

Answer: Not necessarily.

If the conditions for escape analysis are met, an object can be allocated on the stack.Reduce heap memory allocation and GC pressure.Since stack memory is limited, if the object meets the conditions for scalar replacement,Further, a surgery is performed on the subject to break the whole into parts.The specific method of scalar replacement is: JVM will further break up the object and decompose it into several member variables used by this method.Thus, the goal of better utilizing stack memory and registers is achieved.

How to allocate objects on the heap to the stack requires the use of escape analysis.

This is a cross-function global data flow analysis algorithm that can effectively reduce synchronization load and memory heap allocation pressure in Java programs. Through escape analysis, the Java Hotspot compiler can analyze the usage scope of a new object reference and decide whether to allocate the object to the heap.

The basic behavior of escape analysis is to analyze the dynamic scope of objects:

  1. When an object is defined in a method and is only used within the method, it is considered that no escape occurs.
  2. When an object is defined in a method and then referenced by an external method, it is considered to have escaped, for example, when it is passed as a call parameter to another place.

In the computer language compiler optimization principle, escape analysis refers to the method of analyzing the dynamic range of pointers, which is related to the pointer analysis and shape analysis of the compiler optimization principle. When a variable (or object) is allocated in a method, its pointer may be returned or globally referenced, so that it will be referenced by other methods or threads. This phenomenon is called pointer (or reference) escape. In layman's terms, if an object's pointer is referenced by multiple methods or threads, then we call the pointer (or object) of this object escape (because at this time, the object escapes the local scope of the method or thread).

What is escape analysis?

Brief description: "Escape analysis: A static analysis that determines the dynamic scope of a pointer. It can analyze where in the program the pointer can be accessed." In the context of JVM's just-in-time compilation, escape analysis will determine whether a newly created object escapes.

The basis for the just-in-time compiler to determine whether an object escapes is: one is whether the object is stored in the heap (static field or instance field of an object in the heap), and the other is whether the object is passed into unknown code.

Escape Analysis is a cutting-edge optimization technology in the current Java virtual machine. Like type inheritance relationship analysis, it is not a means of directly optimizing the code, but an analysis technology that provides a basis for other optimization methods.

Escape Analysis: It is a very important JIT optimization technology, which is used to determine whether an object can be accessed outside the method, that is, escape the scope of the method. Escape analysis is a step of the JIT compiler. Through JIT, we can determine which objects can be restricted to use inside the method and will not escape to the outside, and then we can optimize them, such as allocating them on the stack instead of the heap, or performing scalar replacement to break an object into multiple basic types for storage. It is a cross-function global data flow analysis algorithm that can effectively reduce synchronization loads and memory heap allocation and garbage collection pressure in Java programs. Through escape analysis, the Java Hotspot compiler can analyze the scope of use of a new object's reference and decide whether to allocate the object to the heap.

Escape analysis mainly targets local variables to determine whether objects allocated on the heap escape the scope of the method. It is related to pointer analysis and shape analysis of compiler optimization principles. When a variable (or object) is allocated in a method, its pointer may be returned or globally referenced, so that it will be referenced by other methods or threads. This phenomenon is called pointer (or reference) escape. In layman's terms, if a pointer to an object is referenced by multiple methods or threads, then we say that the pointer of this object has escaped. Reasonable design of code structure and data usage can better utilize escape analysis to optimize program performance. We can also reduce the overhead of objects allocated on the heap and improve memory utilization through escape analysis.

Escape analysis is a technique used to determine whether an object escapes from the external scope of a method during its life cycle. In Java development, escape analysis is used to determine the life cycle and scope of an object so that corresponding optimizations can be performed to improve program performance and memory utilization efficiency.

Once an object is created, it can be used inside a method, passed to other methods or threads, and continue to exist outside the method. If the object does not escape the scope of the method, the JVM can allocate it on the stack instead of the heap, thus avoiding the overhead of heap memory allocation and garbage collection.

  1. 关于逃逸分析的论文在1999年就已经发表,但直到Sun JDK 1.6才实现了逃逸分析,而且直到现在这项优化尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗。如果要完全准确的判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。这是一个相对高耗时的过程,如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。还有一点是,基于逃逸分析的一些优化手段,如上面提到的“栈上分配”,由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化。
  2. 在测试结果中,实施逃逸分析后的程序在MicroBenchmarks中往往能运行出不错的成绩,但是在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即使编译的收益)有所下降,所以在很长的一段时间里,即使是Server Compiler,也默认不开启逃逸分析(在JDK 1.6 Update 23的Server Compiler中才开始默认开启了逃逸分析),甚至在某些版本(如JDK 1.6 Update 18)中还曾经短暂的完全禁止了这项优化。
  3. 如果有需要,并且确认对程序运行有益,用户可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析,开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,用户可以使用参数-XX:EliminateAllocations来开启标量替换,使用+XX:+EliminateLocks来开启同步消除,使用参数-XX:PrintEliminateAllocations查看标量的替换情况。
  4. 尽管目前逃逸分析的技术仍不是十分成熟,但是他却是即时编译器优化技术的一个重要的方向,在今后的虚拟机中,逃逸分析技术肯定会支撑起一系列使用有效的优化技术。

The basic principles of escape analysis

The basic principle of JVM escape analysis is to determine the escape status of an object through static and dynamic analysis methods.

In the Java compilation system, a Java source code file needs to go through two stages of compilation in the process of becoming a computer-executable machine instruction:

The first compilation refers to the front-end compiler.java fileConvert to.class files(Bytecode file). The front-end compiler product can be Javac of JDK or the incremental compiler in Eclipse JDT.

In the second compilation stage, JVM translates the bytecode into corresponding machine instructions by interpreting it, reading the bytecode one by one, and interpreting and translating it into machine code one by one.

Obviously, due to the intermediate process of interpretation, its execution speed will inevitably be much slower than the executable binary bytecode program. This is the function of the traditional JVM interpreter.

How to eliminate middlemen and improve efficiency?

In order to solve this efficiency problem, JIT (Just In Time Compiler) technology was introduced.

After the introduction of JIT technology, Java programs are still interpreted and executed through the interpreter, that is, the main body is still interpreted and executed, but the intermediate links are partially removed.

JIT Compiler (Just-in-time Compiler) Real-time compilation. The earliest Java implementation plan was a set of interpreters that translated each Java instruction into an equivalent microprocessor instruction and executed it in sequence according to the order of the translated instructions. Since a Java instruction may be translated into dozens or even dozens of equivalent microprocessor instructions, this mode of execution is quite slow.

How to partially remove the intermediate links?

When JVM finds that a method or code block is running very frequently, it will consider it as "hot spot code". Then JIT will translate part of the "hot spot code" into machine code related to the local machine, optimize it, and then cache the translated machine code for next use.

Where is the translated machine code cached? This cache is called Code Cache. It can be seen that the means of achieving high concurrency in JVM and WEB applications are similar, and they still use a cache architecture.

When the JVM encounters the same hot code next time, it skips the intermediate link of interpretation, loads the machine code directly from the Code Cache, and executes it directly without recompiling.

Therefore, the overall goal of JIT is to discover hotspot codes. Hotspot codes become the key to improving performance. This is how the hotspot JVM got its name. Identifying hotspot codes is written in its name as a lifelong pursuit.

Therefore, the overall strategy of JVM is:

  • For the infrequently used codes that occupy most of the time, we do not need to spend time compiling them into machine code, but instead run them in an interpreted execution mode;

  • On the other hand, for hotspot codes that only occupy a small part, we can compile them into machine codes to achieve the ideal running speed.

The emergence of JIT (Just-in-time compilation) and the difference between interpreters

(1) The interpreter interprets bytecode into machine code. Even if the same bytecode is encountered next time, it will still perform repeated interpretation.

(2) JIT compiles some bytecodes into machine code and stores it in the Code Cache. The next time the same code is encountered, it is executed directly without recompilation.

(3) The interpreter interprets the bytecode into machine code that is common to all platforms.

(4) JIT will generate platform-specific machine code based on the platform type.

The JVM contains multiple just-in-time compilers, mainly C1 and C2, and Graal (experimental).

Multiple just-in-time compilers optimize bytecodes and generate machine code

  • C1 will perform simple and reliable optimization on bytecode, including method inlining, devirtualization, redundancy elimination, etc., with a faster compilation speed. You can force C1 compilation through -client
  • C2 will perform aggressive optimizations on the bytecode, including branch frequency prediction, synchronous erasure, etc. You can force C2 compilation with -server

JVM divides the execution state into five levels:

  • Layer 0, Interpreter

  • Tier 1, compiled and executed using the C1 just-in-time compiler (without profiling)

  • Layer 2, compiled and executed using the C1 just-in-time compiler (with basic profiling)

  • 3-tier, compiled and executed using the C1 just-in-time compiler (with full profiling)

  • 4 layers, compiled and executed using the C2 just-in-time compiler

JVM will not enable C2 directly, but will first compile the collection program's running status through C1, and then determine whether to enable C2 based on the analysis results.

In the layered compilation mode, the virtual machine execution state is divided into 5 layers from simple to complex and from fast to slow.

During compilation, JIT will perform many code optimizations in addition to caching hot code to speed it up.

One of the optimization purposes isReduce memory heap allocation pressureOne important technique in JIT optimization is called escape analysis. Based on escape analysis, the just-in-time compiler will optimize the code as follows during compilation:

  • Lock elimination: When a lock object is locked by only one thread, the just-in-time compiler removes the lock
  • Stack allocation: When an object does not escape, it will be directly allocated on the stack. As the thread is recycled, since a large amount of JVM code is heap allocated, the JVM currently does not support stack allocation, but uses scalar replacement
  • Scalar replacement: When an object does not escape, the current object will be broken into several local variables and allocated in the local variable table of the virtual machine stack

1. Static analysis is performed at compile time

It checks the static structure of the code to determine whether an object is likely to escape. For example, when an object is assigned to a member variable of a class or returned to an external method, it can be determined that the object escapes.

2. Dynamic analysis is performed at runtime

It determines whether an object escapes by observing the behavior of method calls and object references. For example, when an object is referenced by multiple threads, it can be determined that the object escapes.

Escape analysis performs a deep analysis of the code to determine whether an object escapes from the outer scope of a method during its lifetime. If the object does not escape, the JVM can allocate it on the stack instead of the heap.

Escape status: global escape, parameter escape, no escape

An object has three escape states: global escape, parameter escape, and no escape.

Global escape(GlobalEscape): The scope of an object escapes the current method or current thread.

Generally there are the following scenarios:
① The object is a static variable
② The object is an object that has escaped
③ The object is used as the return value of the current method

Parameter escape(ArgEscape): That is, an object is passed as a method parameter or referenced by a parameter, but no global escape occurs during the call. This state is determined by the bytecode of the called method.

No escape: That is, the object in the method does not escape.

The escape state sample code is as follows:

  1. public class EscapeAnalysisTest {
  2. public static Object globalVariableObject;
  3. public Object instanceObject;
  4. public void globalVariableEscape(){
  5. globalVariableObject = new Object(); // 静态变量,外部线程可见,发生逃逸
  6. }
  7. public void instanceObjectEscape(){
  8. instanceObject = new Object(); // 赋值给堆中实例字段,外部线程可见,发生逃逸
  9. }
  10. public Object returnObjectEscape(){
  11. return new Object(); // 返回实例,外部线程可见,发生逃逸
  12. }
  13. public void noEscape(){
  14. Object noEscape = new Object(); // 仅创建线程可见,对象无逃逸
  15. }
  16. }

Ways of escape: method escape and thread escape


1. Method escape: In a method body, a local variable is defined, and it may be referenced by an external method, such as being passed to a method as a call parameter, or being directly returned as an object. Alternatively, it can be understood as an object jumping out of a method.

Method escapes include:

  • By calling parameters, the object address is passed to other methods.
  • The object returns the object pointer to other methods through the return statement
  • etc.
  1. 我们可以用下面的代码来表示这个现象。
  2. //StringBuffer对象发生了方法逃逸
  3. public static StringBuffer createStringBuffer(String s1, String s2) {
  4. StringBuffer sb = new StringBuffer();
  5. sb.append(s1);
  6. sb.append(s2);
  7. return sb;
  8. }
  9. 上面的例子中,StringBuffer 对象通过return语句返回。
  10. StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
  11. 甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
  12. 不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
  13. 具体的代码如下:
  14. // 非方法逃逸
  15. public static String createString(String s1, String s2) {
  16. StringBuffer sb = new StringBuffer();
  17. sb.append(s1);
  18. sb.append(s2);
  19. return sb.toString();
  20. }
  21. 可以看出,想要逃逸方法的话,需要让对象本身被外部调用,或者说, 对象的指针,传递到了 方法之外。

How to quickly determine whether escape analysis has occurred? Just check whether the new object entity is called outside the method.

  1. public class EscapeAnalysis {
  2.  
  3.     public EscapeAnalysis obj;
  4.  
  5.     /**
  6.      * 方法返回EscapeAnalysis对象,发生逃逸
  7.      * @return
  8.      */
  9.     public EscapeAnalysis getInstance() {
  10.         return obj == null ? new EscapeAnalysis():obj;
  11.     }
  12.  
  13.     /**
  14.      * 为成员属性赋值,发生逃逸
  15.      */
  16.     public void setObj() {
  17.         this.obj = new EscapeAnalysis();
  18.     }
  19.  
  20.     /**
  21.      * 对象的作用于仅在当前方法中有效,没有发生逃逸
  22.      */
  23.     public void useEscapeAnalysis() {
  24.         EscapeAnalysis e = new EscapeAnalysis();
  25.     }
  26.  
  27.     /**
  28.      * 引用成员变量的值,发生逃逸
  29.      */
  30.     public void useEscapeAnalysis2() {
  31.         EscapeAnalysis e = getInstance();
  32.     }
  33. }

2. Thread escape: The object is accessed by another thread, such as being assigned to an instance variable and accessed by another thread. The object escapes the current thread.

Optimization strategies for escape analysis

Escape analysis can bring the following optimization strategies to Java programs: stack allocation, synchronization elimination, scalar replacement, and method inlining;

Escape analysis related parameters:

  1. -XX:+DoEscapeAnalysis 开启逃逸分析
  2. -XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。
  3. -XX:+EliminateAllocations 开启标量替换
  4. -XX:+EliminateLocks 开启同步消除
  5. -XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。

1. Stack Allocation

Escape analysis can determine which objects will not escape the scope of the method and allocate these objects on the stack instead of the heap. Objects allocated on the stack are created and destroyed during the life cycle of the method call, without the need for garbage collection, thereby improving the execution efficiency of the program.

Generally speaking, objects that do not escape take up a lot of space. If the space on the stack can be used, a large number of objects will be destroyed when the method ends, reducing GC pressure.

        Allocation ideas on the stackStack allocation is an optimization technology provided by JVM.
The idea is:

  1. For thread-private objects (objects that cannot be accessed by other threads), they can be allocated to stack memory instead of heap memory, which is a solution to replace poly variables with scalars.
  2. The advantage of allocating to the stack is that it can be automatically destroyed after the method ends, without the need for GC intervention, improving system performance
  3. For a large number of scattered objects, stack allocation provides a good object allocation strategy. Stack allocation is fast and can effectively avoid the negative impact of GC recovery.

Problem: Since the stack memory is relatively small, large objects cannot and are not suitable for stack allocation.
        Enable stack allocation
Allocation on the stack is based on escape analysis and scalar replacement, so escape analysis and scalar replacement must be enabled. Of course, JDK1.8 is enabled by default.

  1. 开启逃逸分析:-XX:+DoEscapeAnalysis
  2. 关闭逃逸分析:-XX:-DoEscapeAnalysis
  3. 显示分析结果:-XX:+PrintEscapeAnalysis
  4. 开启标量替换:-XX:+EliminateAllocations
  5. 关闭标量替换:-XX:-EliminateAllocations
  6. 显示标量替换详情:-XX:+PrintEliminateAllocations

Example of stack allocation:

  1. 示例1
  2. import java.lang.management.ManagementFactory;
  3. import java.util.List;
  4. /**
  5. * 逃逸分析优化-栈上分配
  6. * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
  7. * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。
  8. *虚拟机参数设置如下,表示做了逃逸分析 消耗时间在10毫秒以下
  9. * -server -Xmx10M -Xms10M
  10. -XX:+DoEscapeAnalysis -XX:+PrintGC
  11. *
  12. *虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上
  13. * -server -Xmx10m -Xms10m
  14. -XX: -DoEscapeAnalysis -XX:+PrintGC
  15. * @author 734621
  16. *
  17. */
  18. public class OnStack{
  19. public static void alloc(){
  20. byte[] b=new byte[2];
  21. b[0]=1;
  22. }
  23. public static void main(String [] args){
  24. long b=System.currentTimeMillis();
  25. for(int i=0;i<100000000;i++){
  26. alloc();
  27. }
  28. long e=System.currentTimeMillis();
  29. System.out.println("消耗时间为:" + (e - b));
  30. List<String> paramters = ManagementFactory.getRuntimeMXBean().getInputArguments();
  31. for(String p : paramters){
  32. System.out.println(p);
  33. }
  34. }
  35. }
  36. 加逃逸分析的结果
  37. [GC (Allocation Failure) 2816K->484K(9984K), 0.0013117 secs]
  38. 消耗时间为:7
  39. -Xmx10m
  40. -Xms10m
  41. -XX:+DoEscapeAnalysis
  42. -XX:+PrintGC
  43. 没有加逃逸分析的结果如下:
  44. [GC (Allocation Failure) 3320K->504K(9984K), 0.0003174 secs]
  45. [GC (Allocation Failure) 3320K->504K(9984K), 0.0002524 secs]
  46. 消耗时间为:1150
  47. -Xmx10m
  48. -Xms10m
  49. -XX:-DoEscapeAnalysis
  50. -XX:+PrintGC
  51. 以上测试可以看出,栈上分配可以明显提高效率: 效率是不开启的1150/7= 160
  52. 示例2
  53. 我们通过举例来说明 开启逃逸分析 和 未开启逃逸分析时候的情况
  54. class User {
  55. private String name;
  56. private String age;
  57. private String gender;
  58. private String phone;
  59. }
  60. public class StackAllocation {
  61. public static void main(String[] args) throws InterruptedException {
  62. long start = System.currentTimeMillis();
  63. for (int i = 0; i < 100000000; i++) {
  64. alloc();
  65. }
  66. long end = System.currentTimeMillis();
  67. System.out.println("花费的时间为:" + (end - start) + " ms");
  68. // 为了方便查看堆内存中对象个数,线程sleep
  69. Thread.sleep(10000000);
  70. }
  71. private static void alloc() {
  72. // 未发生逃逸
  73. User user = new User();
  74. }
  75. }
  76. 设置JVM参数,表示未开启逃逸分析
  77. -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
  78. 花费的时间为:664 ms
  79. 然后查看内存的情况,发现有大量的User存储在堆中
  80. 开启逃逸分析
  81. -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
  82. 然后查看运行时间,我们能够发现花费的时间快速减少,同时不会发生GC操作
  83. 花费的时间为:5 ms
  84. 在看内存情况,我们发现只有很少的User对象,说明User未发生逃逸,因为它存储在栈中,随着栈的销毁而消失。

By comparison, we can see

  • Enable stack allocation and allocate non-escaped objects in stack memory, which is obviously more efficient.
  • After closing the allocation on the stack, GC performs garbage collection frequently.

2. Lock Elimination

Escape analysis can detect that certain objects are only accessed by a single thread and will not escape to other threads. Therefore, unnecessary synchronization operations can be eliminated, reducing the execution overhead of multi-threaded programs.

Synchronous locks are very performance-intensive, so when the compiler determines that an object has not escaped, it removes the object's synchronization lock. JDK1.8 has synchronized locks enabled by default, but this is based on enabling escape analysis.

  1. -XX:+EliminateLocks #开启同步锁消除(JVM默认状态)
  2. -XX:-EliminateLocks #关闭同步锁消除
  1. 通过示例: 明显可以看到“逃逸分析和锁消除” 对性能的提升
  2. public void testLock(){
  3. long t1 = System.currentTimeMillis();
  4. for (int i = 0; i < 100_000_000; i++) {
  5. locketMethod();
  6. }
  7. long t2 = System.currentTimeMillis();
  8. System.out.println("耗时:"+(t2-t1));
  9. }
  10. public static void locketMethod(){
  11. EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
  12. synchronized(escapeAnalysis) {
  13. escapeAnalysis.obj2="abcdefg";
  14. }
  15. }
  16. 设置JVM参数,开启逃逸分析, 耗时:
  17. java -Xmx64m -Xms64m -XX:+DoEscapeAnalysis
  18. 设置JVM参数,关闭逃逸分析, 耗时:
  19. java -Xmx64m -Xms64m -XX:-DoEscapeAnalysis
  20. 设置JVM参数,关闭锁消除,再次运行
  21. java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateLocks
  22. 设置JVM参数,开启锁消除,再次运行
  23. java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateLocks

The cost of thread synchronization is quite high, and the consequence of synchronization is reduced concurrency and performance.

When dynamically compiling a synchronized block, the JIT compiler can use escape analysis to determine whether the lock object used by the synchronized block can only be accessed by one thread and has not been released to other threads. If not, the JIT compiler will cancel the synchronization of this part of the code when compiling the synchronized block. This can greatly improve concurrency and performance. This process of canceling synchronization is called synchronization elision, also known as lock elimination.

  1. 例如下面的代码
  2. public void f() {
  3.     Object hellis = new Object();
  4.     synchronized(hellis) {
  5.         System.out.println(hellis);
  6.     }
  7. }
  8. 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
  9. public void f() {
  10.     Object hellis = new Object();
  11.     System.out.println(hellis);
  12. }
  13. 我们将其转换成字节码,此处发现,还是有同步锁的身影,是因为优化是在编译阶段的,在加载进内存后发生。

3. Scalar Replacement

Escape analysis can split an object into multiple scalars, such as basic types or other objects, and allocate them in different locations. This can reduce memory fragmentation and object access overhead and improve memory utilization efficiency.

First of all, we need to understand scalars and aggregates. References to basic types and objects can be understood as scalars, which cannot be further decomposed. Quantities that can be further decomposed are aggregates, such as objects.

An object is an aggregate, which can be further decomposed into scalars, decomposing its member variables into discrete variables. This is called scalar substitution.

In this way, if an object does not escape, it does not need to be created at all. Only the member scalars used by it will be created on the stack or register, saving memory space and improving application performance.

Scalar replacement is also enabled by default in JDK1.8, but it also requires that escape analysis be enabled.

A scalar is a data that cannot be broken down into smaller data. The primitive data type in Java is a scalar.

In contrast, data that can be decomposed is called an aggregate. Objects in Java are aggregates because they can be decomposed into other aggregates and scalars.

  1. public static void main(String args[]) {
  2.     alloc();
  3. }
  4. class Point {
  5.     private int x;
  6.     private int y;
  7. }
  8. private static void alloc() {
  9.     Point point = new Point(1,2);
  10.     System.out.println("point.x" + point.x + ";point.y" + point.y);
  11. }
  12. 以上代码,经过标量替换后,就会变成
  13. private static void alloc() {
  14.     int x = 1;
  15.     int y = 2;
  16.     System.out.println("point.x = " + x + "; point.y=" + y);
  17. }

In the JIT phase, if an object is found to not be accessed by the outside world after escape analysis, then after JIT optimization, the object will be disassembled into several member variables contained in it to replace them. This process is called scalar replacement.
As you can see, after the escape analysis, the aggregate Point was found not to have escaped, so it was replaced with two scalars. So what are the benefits of scalar replacement? It can greatly reduce the heap memory usage. Because once there is no need to create an object, there is no need to allocate heap memory. Scalar replacement provides a good foundation for stack allocation.

Escape Analysis Testing

  1. 逃逸分析测试
  2. 代码如下,大致思路就是 for 循环 1 亿次,循环体内调用外部的 allot() 方法,而 allot() 方法的作用就是简单创建一个对象,但是这个对象是内部的,所以是未逃逸的,所以理论上 JVM 是会进行优化的,我们拭目以待。并且我们会对比开启和关闭逃逸分析之后各自程序的运行时间:
  3. /**
  4. * @ClassName: EscapeAnalysisTest
  5. * @Description: http://www.jetchen.cn 逃逸分析 demo
  6. * @Author: Jet.Chen
  7. * @Date: 2020/11/23 14:26
  8. * @Version: 1.0
  9. **/
  10. public class EscapeAnalysisTest {
  11. public static void main(String[] args) {
  12. long t1 = System.currentTimeMillis();
  13. for (int i = 0; i < 100000000; i++) {
  14. allot();
  15. }
  16. long t2 = System.currentTimeMillis();
  17. System.out.println(t2-t1);
  18. }
  19. private static void allot() {
  20. Jet jet = new Jet();
  21. }
  22. static class Jet {
  23. public String name;
  24. }
  25. }
  26. 上面就是我们进行逃逸分析测试的代码, mian() 方法末尾有一个线程暂停,目的是为了观察此时 JVM 中的内存情况。
  27. Step 1:测试开启逃逸
  28. 由于环境是 jdk1.8,默认开启了逃逸分析,所以直接运行,得到结果如下,程序耗时 3 毫秒:
  29. 此时线程是处于睡眠状态的,我们观察下内存情况,发现堆内存中一共新建了 11 万个 Jet 对象。
  30. Step 2:测试关闭逃逸
  31. 我们关闭逃逸分析再来运行一次(使用 java -XX:-DoEscapeAnalysis EscapeAnalysisTest 来运行代码即可),得到结果如下,程序耗时 400 毫秒:
  32. 此时我们观察下内存情况,发现堆内存中一共新建了 3 千多万个 Jet 对象。
  33. 所以,无论是从代码的执行时间(3 毫秒 VS 400 毫秒),还是从堆内存中对象的数量(11 万个 VS 3 千万个)来分析,在上述场景下,开启逃逸分析是有正向益的。
  34. Step 3:测试标量替换
  35. 我们测试下开启和关闭 标量替换,如下图:
  36. 由上图我们可以看出,在上述极端场景下,开启和关闭标量替换对于性能的影响也是满巨大的,另外,同时也验证了标量替换功能生效的前提是逃逸分析已经开启,否则没有意义。
  37. Step 4:测试锁消除
  38. 测试锁消除,我们需要简单调整下代码,即给 allot() 方法中的内容加锁处理,如下:
  39. private static void allot() {
  40. Jet jet = new Jet();
  41. synchronized (jet) {
  42. jet.name = "jet Chen";
  43. }
  44. }
  45. 然后我们运行测试代码,测试结果也很明显,在上述场景下,开启和关闭锁消除对程序性能的影响也是巨大的。
  46. /**
  47. * 进行两种测试
  48. * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
  49. * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
  50. *
  51. * 开启逃逸分析 jdk8默认开启
  52. * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
  53. *
  54. * 执行main方法后
  55. * jps 查看进程
  56. * jmap -histo 进程ID
  57. *
  58. */
  59. @Slf4j
  60. public class EscapeTest {
  61. public static void main(String[] args) {
  62. long start = System.currentTimeMillis();
  63. for (int i = 0; i < 500000; i++) {
  64. alloc();
  65. }
  66. long end = System.currentTimeMillis();
  67. log.info("执行时间:" + (end - start) + " ms");
  68. try {
  69. Thread.sleep(Integer.MAX_VALUE);
  70. } catch (InterruptedException e1) {
  71. e1.printStackTrace();
  72. }
  73. }
  74. /**
  75. * JIT编译时会对代码进行逃逸分析
  76. * 并不是所有对象存放在堆区,有的一部分存在线程栈空间
  77. * Ponit没有逃逸
  78. */
  79. private static String alloc() {
  80. Point point = new Point();
  81. return point.toString();
  82. }
  83. /**
  84. *同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
  85. */
  86. public void append(String str1, String str2) {
  87. StringBuffer stringBuffer = new StringBuffer();
  88. stringBuffer.append(str1).append(str2);
  89. }
  90. /**
  91. * 标量替换
  92. *
  93. */
  94. private static void test2() {
  95. Point point = new Point(1,2);
  96. System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
  97. // int x=1;
  98. // int y=2;
  99. // System.out.println("point.x="+x+"; point.y="+y);
  100. }
  101. }
  102. @Data
  103. @AllArgsConstructor
  104. @NoArgsConstructor
  105. class Point{
  106. private int x;
  107. private int y;
  108. }

4. Method Inlining

Escape analysis can determine that some method calls will not escape the scope of the current method. Therefore, these methods can be inlined to reduce the overhead of method calls and improve the execution efficiency of the program.

Through these optimization strategies, escape analysis can help JVM better optimize code, reduce garbage collection overhead, improve program execution efficiency and responsiveness, and reduce memory usage.
 

Practical application scenarios

Escape analysis has a wide range of application scenarios in actual Java applications. The following are some common application scenarios:

  1. When an object is passed as a method parameter, escape analysis can determine whether the object escapes and thus decide whether the object is allocated on the heap or on the stack.
    1. When an object is used as a method return value, escape analysis can determine whether the object escapes, thereby determining whether the object is allocated on the heap or on the stack.
    1. When an object is shared by threads, escape analysis can determine whether the object escapes and thus decide whether synchronization operations are required.
    1. When temporary objects are created in a loop, escape analysis can determine whether the object escapes and thus decide whether the object needs to be created and destroyed frequently.

Disadvantages of escape analysis

The paper on escape analysis was published in 1999, but it was not implemented until JDK1.6, and this technology is not very mature even now.

The fundamental reason is that there is no guarantee that the performance cost of escape analysis will be higher than its cost. Although escape analysis can perform scalar replacement, stack allocation, and lock elimination, escape analysis itself also requires a series of complex analyses, which is actually a relatively time-consuming process.

An extreme example is that after the escape analysis, it is found that no object does not escape. Then the escape analysis process is wasted.

Although this technology is not very mature, it is also a very important means of just-in-time compiler optimization technology. Note that there are some opinions that through escape analysis, the JVM will allocate objects that will not escape on the stack. This is theoretically feasible, but it depends on the choice of the JVM designer. As far as I know, Oracle Hotspot JVM does not do this. This has been explained in the documents related to escape analysis, so it is clear that all object instances are created on the heap.

Currently, many books are still based on versions before JDK7. JDK has changed a lot. The cache of intern strings and static variables used to be allocated in the permanent generation, which has been replaced by the metadata area. However, the cache of intern strings and static variables are not transferred to the metadata area, but directly allocated on the heap, so this also conforms to the conclusion of the previous point: object instances are allocated on the heap. The above example is faster because of scalar replacement.

Benefits of Escape Analysis


If an object does not escape within a method or thread (or if it is determined that it does not escape after escape analysis), the following optimizations can be performed:

Allocation on the stack:


Generally speaking, objects that do not escape take up a lot of space. If the space on the stack can be used, a large number of objects will be destroyed when the method ends, reducing GC pressure.


Synchronous elimination:


If there is a synchronization lock on the method of the class you defined, but only one thread is accessing it during runtime, the machine code after escape analysis will run without the synchronization lock.


Scalar replacement:


The primitive data types in the Java virtual machine (numeric types such as int, long, and reference types, etc.) cannot be further decomposed, and they can be called scalars. In contrast, if a data can be further decomposed, it is called an aggregate. The most typical aggregate in Java is an object. If escape analysis proves that an object will not be accessed externally and the object is decomposable, then when the program is actually executed, it may not create the object, but directly create several member variables used by the method instead. The disassembled variables can be analyzed and optimized separately. After the attributes are flattened, there is no need to establish relationships through reference pointers. They can be stored continuously and compactly, which is more friendly to various storages and can save a lot of performance loss caused by data handling during execution. At the same time, space can be allocated on the stack frame or register separately, and the original object does not need to be allocated space as a whole.

summary


The young generation is the area where objects are born, grow, and die. An object is created and used here, and is finally collected by the garbage collector and ends its life.

The old generation is used to store objects with long life cycles, which are usually Java objects copied from the survivor area. Of course, there are special cases. We know that ordinary objects will be allocated on TLAB; if the object is large, the JVM will try to allocate it directly in other locations in Eden; if the object is too large and cannot find a long enough continuous free space in the new generation, the JVM will allocate it directly to the old generation. When GC only occurs in the young generation, the act of recycling young generation objects is called MinorGc.

When GC occurs in the old generation, it is called MajorGC or FullGC. Generally, the frequency of MinorGC is much higher than that of MajorGC, that is, the frequency of garbage collection in the old generation will be much lower than that in the young generation.

JVM escape analysis uses static and dynamic analysis methods to determine whether an object is likely to escape from the scope of a method. It can help JVM optimize code and improve the performance and memory utilization efficiency of Java programs.

The optimization strategies of escape analysis include stack allocation, synchronization elimination, scalar replacement, and method inlining. These optimization strategies can reduce the overhead of garbage collection, improve the execution efficiency and responsiveness of the program, and reduce memory usage.

refer to:

https://zhuanlan.zhihu.com/p/693382698

JVM-Heap-Escape Analysis-08-CSDN Blog

JIT memory escape analysis_java close scalar replacement-CSDN blog

View all JVM configuration parameter values

java -XX:+PrintFlagsFinal  #输出打印所有参数jvm参数

insert image description here
insert image description here
insert image description here
insert image description here