Some time ago, to the planet’s ball friends special code an article “in-depth analysis of Java compilation principle”, which in-depth introduction of Java in javAC compilation and JIT compilation difference and principle. Besides the function of cache, JIT compilation will also optimize the code, such as escape analysis, lock elimination, lock expansion, method inlining, null check elimination, type detection elimination, common subexpression elimination and so on.

After reading this part, I became very interested in JVM and went back to study it myself. During the learning process, I encountered a small problem about Java memory allocation. So he had a simple communication with me on wechat. It mainly involves Java heap and stack, array memory allocation, escape analysis, compiler optimization and other technologies and principles. This article is also about this part of the knowledge sharing.

JVM memory allocation policy

The memory structure of the JVM and how it is allocated is not the focus of this article, but rather a brief review. Here’s what we know:

1. According to the Java Virtual Machine specification, the memory managed by the Java Virtual machine includes method area, virtual machine stack, local method stack, heap, program counter, etc.

2. We generally think of run-time data stores in the JVM as consisting of stacks and heaps. The stack referred to here actually refers to the virtual machine stack, or the local variable table in the virtual stack.

3, the stack to store some of the basic types of variable data (int/short/long/byte/float/double/Boolean/char) and object references.

4. The heap mainly holds objects, that is, objects created with the new keyword.

5, Array reference variables are stored in stack memory, array elements are stored in heap memory.

In Understanding the Java Virtual Machine, there is a description of Java heap memory:

However, as JIT compile-time advances and escape analysis techniques mature, on-stack allocation and scalar replacement optimization techniques will lead to subtle changes, and all objects allocated to the heap will become less “absolute.”

This is just a brief mention, not an in-depth analysis, many people see here because of JIT, escape analysis, etc., do not understand the meaning of the above paragraph.

PS: here is the default we all understand what is JIT, do not know the friends can first Google to understand, or join my knowledge planet, read the ball friends exclusive article.

In fact, the JIT does a lot of optimizations to the code during compilation. Some of these optimizations are designed to reduce heap allocation stress, and an important technique is called escape analysis.

Escape analysis

Escape Analysis is one of the most advanced optimization techniques in Java virtual machines. This is a cross-function global data flow analysis algorithm that can effectively reduce the synchronization load and memory heap allocation stress in Java programs. Through escape analysis, the Java Hotspot compiler can figure out how far a new object’s references are used to determine whether to allocate the object to the heap.

The basic behavior of escape analysis is analyzing object dynamic scope: when an object is defined in a method, it may be referenced by an external method, such as passing it elsewhere as a call parameter, called method escape.

Such as:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}
Copy the code

A StringBuffer sb is an internal variable of the method. In the above code, sb is returned directly, so that the StringBuffer can be changed by other methods, so that its scope is not just inside the method, although it is a local variable that escapes outside the method. It may even be accessed by an external thread, such as an instance variable assigned to a class variable or accessible from another thread, called thread escape.

If you want StringBuffer sb not to escape the method, you can write:

public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}
Copy the code

If a StringBuffer is not returned directly, it will not escape the method.

Using escape analysis, the compiler can optimize code as follows:

One, synchronous ellipsis. If an object is found to be accessible only from one thread, operations on the object can be performed without regard to synchronization.

Convert heap allocation to stack allocation. If an object is allocated in a subroutine so that Pointers to it never escape, the object may be a candidate for stack allocation, not heap allocation.

Separate objects or scalar substitutions. Some objects may be accessible without needing to exist as a continuous memory structure, and some (or all) of the object may be stored not in memory, but in CPU registers.

The above content about synchronous ellipsis, I have been introduced in “In-depth understanding of multi-threading (five) — Java VIRTUAL machine lock optimization technology”, that is, lock optimization lock elimination technology, depends on escape analysis technology.

This article focuses on the second use of escape analysis: converting heap allocation to stack allocation.

In the above three optimizations, memory allocation on the stack actually relies on scalar substitution. Since it is not the focus of this article, I will not expand on it here. If you’re interested, I’ll write a whole article about escape analysis.

-xx :+DoEscapeAnalysis: indicates that escape analysis is enabled. -xx: -doescapeAnalysis: indicates that escape analysis is enabled. Escape analysis starts from JDK 1.7 by default. To disable escape analysis, specify -xx: -doescapeAnalysis

Object allocates memory on the stack

We know that, in general, memory allocation of objects and array elements is done on heap memory. But as JIT compilers mature, many optimizations make this allocation strategy not absolute. The JIT compiler can then use the results of escape analysis at compile time to determine whether an object’s memory allocation can be converted from the heap to the stack.

Let’s look at the following code:

public static void main(String[] args) { long a1 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { alloc(); } long a2 = system.currentTimemillis (); System.out.println("cost " + (a2 - a1) + " ms"); Sleep try {thread.sleep (100000); } catch (InterruptedException e1) { e1.printStackTrace(); } } private static void alloc() { User user = new User(); } static class User { }Copy the code

The code is simple: create a million User objects in your code using a for loop.

We define the User object in the alloc method, but we do not refer to it outside the method. That is, the object does not escape from alloc. After JIT escape analysis, its memory allocation can be optimized.

We specify the following JVM parameters and run them:

-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 
Copy the code

After the program prints cost XX ms and before the code runs, we use the [jmap][1] command to see how many User objects are in the current heap memory:

➜ ~ JPS 2809 StackAllocTest 2810 JPS ➜ ~ jmap-histo 2809 num #instances #bytes class name ---------------------------------------------- 1: 524 87282184 [I 2: 1000000 16000000 StackAllocTest$User 3: 6806 2093136 [B 4: 8006 1320872 [C 5: 4188 100512 java.lang.String 6: 581 66304 java.lang.ClassCopy the code

As you can see from the jmap execution results above, the heap created a total of 1 million StackAllocTest$User instances.

With escape analysis turned off (-xx: -doescapeAnalysis), the User object created in the alloc method does not escape outside the method, but is still allocated in heap memory. That is, if there were no JIT compiler optimization and no escape analysis techniques, this would normally be the case. That is, all objects are allocated to heap memory.

Next, we turn on escape analysis and execute the above code.

-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 
Copy the code

After the program prints cost XX ms and before the code runs out, we use the jmap command to see how many User objects are currently in the heap:

➜ ~ JPS 709 2858 Launcher 2859 StackAllocTest 2860 JPS ➜ ~ jmap-histo 2859 num #instances #bytes class name ---------------------------------------------- 1: 524 101944280 [I 2: 6806 2093136 [B 3: 83619 1337904 StackAllocTest$User 4: 8006 1320872 [C 5: 4188 100512 java.lang.String 6: 581 66304 java.lang.ClassCopy the code

As you can see from the print above, with escape analysis enabled (-xx :+DoEscapeAnalysis), there are only over 80,000 StackAllocTest$User objects in the heap memory. This means that after JIT optimization, the number of objects allocated in heap memory decreases from 1 million to 80,000.

In addition to the above method of verifying the number of objects through JMAP, the reader can also try to reduce the heap memory and then execute the above code, and analyze according to the number of GC, and also find that after the escape analysis is enabled, the number of GC is significantly reduced during the run. Because many heap allocations are optimized to stack allocations, GC times are significantly reduced.

conclusion

So, if someone asks you in the future: Do all objects and arrays allocate space in the heap?

Not necessarily. As JIT compilers evolve, it is possible that heap allocation will be optimized to stack allocation during compilation if the JIT does escape analysis and finds that some objects do not escape. But this is not absolute. As we saw earlier, not all User objects are not allocated on the heap after escape analysis is turned on.