Escape Analysis & Stack Allocated Objects
In 1999, a paper, Escape Analysis For Java was to appear in the ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages, and Applications. This paper outlined an algorithm to detect whether an object, O
, escaped from the currently executed method, M
, or current thread, T
. This paper laid the groundwork for the HotSpot JVM Escape Analysis optimization introduced in JDK6u14. As of JDKu21, escape analysis became a JVM default for the C2 compiler.
Should you choose to peruse the Internet, you will find that escape analysis (enabled pre-JDKu21 with the flag, -XX:+DoEscapeAnalysis
) tends to increase overall system performance quite substantially. The 1999 paper cites 2% to 23% overall "performance improvement" across several studied systems. Numerous websites show similar (and sometimes far greater) improvements. While I expect escape analysis is likely already enabled in your production environment (and probably to good effect), I would like to explain why you, as a developer, should care.
A Simple Explanation
In prose, the algorithm identifies an object as having one of 3 possible characteristics:
NoEscape
defines an object,O
, as an object that does not escape the method,M
, in which it was created.ArgEscape
defines an object,O
, as an object that is passed as, or referenced from, an argument to a method but does not escape the current thread,T
.GolbalEscape
defines an object,O
, as an object that is accessible by other methods,M
, and threads,T
.
Objects characterised as NoEscape
may be allocated on the stack instead of the heap. Specifically, this means that the object requires no garbage collection and disappears once the method's stack frame is popped. While this applies to any object characterised as NoEscape
, this will most notably reduce the overhead of instantiating high-churn helper objects such as EqualsBuilder
, HashCodeBuilder
, StringBuilder
, and similar temporary objects such as iterators and collections. Important to note, however, is that objects requiring finalizer execution are considered GlobalEscape
and will not be stack-bound.
Objects characterised as ArgEscape
cannot be allocated on the stack; however, because they are accessible only by the currently executing thread, monitors on this object may be eliminated. Because the JVM guarantees sequentially consistent execution within a single thread, all memory synchronization semantics are upheld.
Finally, objects identified as GlobalEscape
objects are not subject to either stack-based allocation or monitor elimination. GlobalEscape
objects remain heap-bound.
A Demonstration
While I expect you, the discerning reader, to performance test your own systems and form your own conclusions as to the impact of escape analysis, I have provided a brief example demonstrating the effects of enabling or disabling escape analysis. Consider an empty class:
public final class EscapeTest
{
public static void main(final String[] args)
{
}
}
Execution of this class, both with and without escape analysis, will provide some baseline numbers of interest:
% java -Xms1G -Xmx1G -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=1 -XX:SurvivorRatio=8 -verbose:gc -XX:+DoEscapeAnalysis EscapeTest
Heap
def new generation total 471872K, used 16778K
eden space 419456K, 4% used
from space 52416K, 0% used
to space 52416K, 0% used
tenured generation total 524288K, used 0K
the space 524288K, 0% used
% java -Xms1G -Xmx1G -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=1 -XX:SurvivorRatio=8 -verbose:gc -XX:-DoEscapeAnalysis EscapeTest
Heap
def new generation total 471872K, used 16778K
eden space 419456K, 4% used
from space 52416K, 0% used
to space 52416K, 0% used
tenured generation total 524288K, used 0K
the space 524288K, 0% used
The heap size in this example is artificially high in attempts of suppressing garbage collection. I have done this for demonstrative purposes only. Observing the garbage collector details, the empty program exhibits no difference in behaviour despite escape analysis. The next example, however, will highlight the effect of escape analysis when creating 15 million objects:
public final class EscapeTest
{
private final int a;
private final int b;
EscapeTest(final int a, final int b)
{
this.a = a;
this.b = b;
}
@Override
public boolean equals(final Object obj)
{
final EscapeTest other = (EscapeTest)obj;
return new EqualsBuilder()
.append(this.a, other.a)
.append(this.b, other.b)
.isEquals();
}
public static void main(final String[] args)
{
final Random random = new Random();
for(int i = 0; i < 5_000_000; i++){
final EscapeTest t1 = new EscapeTest(random.nextInt(), random.nextInt());
final EscapeTest t2 = new EscapeTest(random.nextInt(), random.nextInt());
if(t1.equals(t2)){
System.out.println("Prevent anything from being optimized out.");
}
}
}
}
Traditionally, the EqualsBuilder
and both EscapeTest
instances will be allocated on the heap during each of the 5 million loops. With escape analysis enabled, however, we can reason that t1
and the EqualsBuilder
, both NoEscape
objects, should both be stack allocated:
% java -Xms1G -Xmx1G -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=1 -XX:SurvivorRatio=8 -verbose:gc -XX:+DoEscapeAnalysis EscapeTest
Heap
def new generation total 471872K, used 16778K
eden space 419456K, 4% used
from space 52416K, 0% used
to space 52416K, 0% used
tenured generation total 524288K, used 0K
the space 524288K, 0% used
Notice that with escape analysis enabled, the heap footprint is identical to the empty class: only 4% of newgen is used. When running the same code without escape analysis, the results show a different picture:
% java -Xms1G -Xmx1G -XX:+PrintGCDetails -XX:+UseSerialGC -XX:NewRatio=1 -XX:SurvivorRatio=8 -verbose:gc -XX:-DoEscapeAnalysis EscapeTest
Heap
def new generation total 471872K, used 327176K
eden space 419456K, 78% used
from space 52416K, 0% used
to space 52416K, 0% used
tenured generation total 524288K, used 0K
the space 524288K, 0% used
The new generation sits at 78% utilisation (note that no collections were observed due to the extremely large heap size) suggesting that all 3 objects were heap allocated.
Inlining
Interestingly, the memory profile of creating 15million objects with escape analysis enabled showed the exact same memory profile as running the empty class. While we reasoned that t1
and the EqualsBuilder
were both stack allocated NoEscape
objects, we also reasoned that t2
is a heap allocated ArgEscape
object. The memory profile suggests our reasoning is incorrect and that t2
is, actually, stack allocated. Enabling inline compilation printing helps explain these observations:
74 1 % EscapeTest::main @ 10 (73 bytes)
....
@ 44 EscapeTest::<init> (15 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 52 EscapeTest::equals (38 bytes) inline (hot)
@ 9 org.apache.commons.lang3.builder.EqualsBuilder::<init> (10 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 20 org.apache.commons.lang3.builder.EqualsBuilder::append (25 bytes) inline (hot)
@ 31 org.apache.commons.lang3.builder.EqualsBuilder::append (25 bytes) inline (hot)
@ 34 org.apache.commons.lang3.builder.EqualsBuilder::isEquals (5 bytes) inline (hot)
Notice that due to JVM inlining, t2
does not actually escape the method of its creation (byte code index 44), after all. Specifically, the body of t1.equals(Object)
is inlined into the loop. Indeed, escape analysis considers the escapability of an object after method inlining has been performed. This means that t2
, like t1
, is really categorised as NoEscape
, in this case, and hence may be stack allocated. References are noted in section 8 of Escape Analysis For Java.
So What?
Since you're probably already running escape analysis in your production environment, you may ask why I bother with this article. Simply, I urge you, as a dedicated developer, to freely create and use objects that encapsulate otherwise verbose, redundant, or messy code. This will allow you to communicate your intentions to other developers without worrying about garbage collection overhead or other related (perceived or real) performance hits.
Ultimately, I beseech you to use your understanding of escape analysis to prioritize readability and maintainability over optimization whenever reasonable
References
- Jong-Deok Shoi, Manish Gupta, Mauricio Seffano, Vugranam C. Sreedhar, Sam Midkiff. (November 1, 1999). Escape Analysis For Java.
- HotSpot C2 Escape Analysis Header
- HotSpot C2 Escape Analysis Implementation