Background
In my previous post, I wrote about Escape Analysis and how the JVM can allocate non-escaping objects on the stack rather than on the heap. I immediately got a very interesting question from Caleb Cushing asking if Objects that actually can escape could be optimized anyhow, provided that that escaped object is reasonably contained by the caller.
Read this post and find out the answer!
A Simple Example
Let's assume that we have the following simple Person class:public class Person {
private final String firstName;
private final String middleName;
private final String lastName;
public Person(String firstName, String middleName, String lastName) {
this.firstName = requireNonNull(firstName); // Cannot be null
this.middleName = middleName; // Can be null
this.lastName = requireNonNull(lastName); // Cannot be null
}
public String getFirstName() {
return firstName;
}
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
public String getLastName() {
return lastName;
}
}
Now, if we call the method Person::getMiddleName, it is obvious that the Optional object will escape the method because it is returned by the method and becomes visible to anyone calling the method. Thus, it will be classified as GlobalEscape and must be allocated on the heap. However, this is not necessarily the case. The JVM will sometimes be able to allocate it on the stack, despite the fact that it escapes the method. How is that possible?
What is Escape Analysis (EA)?
Before you read on, I encourage you to read my previous post because it will be more easy to understand what is going on. The post describes the fundamental aspects of EA. How Can GlobalEscape Objects Still Live on the Stack?
It turns out that the C2 compiler is able to do EA not only over single methods, but over larger chunks of code that is inlined by the compiler. Inlining is an optimization scheme where the code is "flattened" to eliminate redundant calls. So, one or several layers of calls are flattened to a sequential list of instructions. The compiler then evaluates EA, not on the individual methods, but on the entire inlined code block. So, even though an object might escape a particular method, it might not be able to escape the larger inlined code block.A Demonstration of Inlined Escape Analysis
public class Main2 {
public static void main(String[] args) throws IOException {
Person p = new Person("Johan", "Sebastian", "Bach");
count(p);
System.gc();
System.out.println("Press any key to continue");
System.in.read();
long sum = count(p);
System.out.println(sum);
System.out.println("Press any key to continue2");
System.in.read();
sum = count(p);
System.out.println(sum);
System.out.println("Press any key to exit");
System.in.read();
}
private static long count(Person p) {
long count = 0;
for (int i = 0; i < 1_000_000; i++) {
if (p.getMiddleName().isPresent()) {
count++;
}
}
return count;
}
}
The code above will create a single instance of a Person and then it will call that Person's getMiddleName() method a large number of times. We will do it in three steps where the first step is just for warming up and then GC away all the objects that were created. The two following steps will not remove anything from the heap and we will be able to examine the heap between each step.We can use the following JVM parameters when we run the code:
-server
-XX:BCEATraceLevel=3
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
-verbose:gc
-XX:MaxInlineSize=256
-XX:FreqInlineSize=1024
-XX:MaxBCEAEstimateSize=1024
-XX:MaxInlineLevel=22
-XX:CompileThreshold=10
-Xmx4g
-Xms4g
After the first run, we get the following heap usage (after the System.gc() call cleaned up all our Optionals)
pemi$ jps | grep Main2
74886 Main2
num #instances #bytes class name
----------------------------------------------
1: 95 42952184 [I
2: 1062 101408 [C
3: 486 55384 java.lang.Class
4: 526 25944 [Ljava.lang.Object;
5: 13 25664 [B
6: 1040 24960 java.lang.String
7: 74 5328 java.lang.reflect.Field
The two following steps gave:
pemi$ jmap -histo 74886 | head
num #instances #bytes class name
----------------------------------------------
1: 95 39019792 [I
2: 245760 3932160 java.util.Optional
3: 1063 101440 [C
4: 486 55384 java.lang.Class
5: 526 25944 [Ljava.lang.Object;
6: 13 25664 [B
7: 1041 24984 java.lang.String
pemi$ jmap -histo 74886 | head
num #instances #bytes class name
----------------------------------------------
1: 95 39019544 [I
2: 245760 3932160 java.util.Optional
3: 1064 101472 [C
4: 486 55384 java.lang.Class
5: 526 25944 [Ljava.lang.Object;
6: 13 25664 [B
7: 1042 25008 java.lang.String
No new Optionals were created between step two and step three and thus, EA was eventually able to eliminate the creation of the Optional instances on the heap even though they escaped the initial method where they were created and returned. This means that we can use an appropriate level of abstraction and still retain performant code.
Conclusions
Escape Analysis can work on several layers in our code. EA can optimize away heap allocation even though objects escapes one or several methods. As with EA in general, we do not get a guarantee that we will get the optimizations we are expecting in all cases.The open-source project Speedment that I am contributing to, often returns Streams containing entities or Optionals. The fact that EA works on several layers makes the application code run faster. The JVM is able to inline code from the Speedment library into the application code itself and then, using EA, temporary return objects are never allocated on the heap. So, Speedment developers can enjoy a nice API while still retaining high performance and low latency
.