Escape analysis and inlining

JVM · lesson 13 of 34 · 4 min read

The JIT inlines small methods, then proves some objects never escape and skips allocating them entirely.

Open this lesson in the learning hub

Key points

  • Inlining copies a small method body into its caller. That removes the call, and lets later optimisations see across the boundary.
  • It is why tiny getters cost nothing once warm, and why enormous methods optimise badly - they blow past the inlining size limits.
  • After inlining, escape analysis asks a simple question: can this object ever be seen outside the method that made it?
  • If it cannot escape, the JIT does scalar replacement. The fields become CPU registers and the object is never allocated at all.
  • It also elides locks on objects only one thread can see. Both optimisations are on by default; you do not need flags.
  • Store it in a field, return it, or pass it to a method too big to inline, and it escapes. Then it costs full price.

Example

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class Main {

    static class Vec {
        final double x, y;
        Vec(double x, double y) { this.x = x; this.y = y; }
        double len() { return Math.sqrt(x * x + y * y); }
    }

    static Vec escapee;                       // a global handle forces a real heap object

    static double sumLocal(int n) {           // the Vec never leaves this method
        double s = 0;
        for (int i = 0; i < n; i++) { s += new Vec(i, i + 1).len(); }
        return s;
    }

    static double sumEscaping(int n) {        // the Vec is published to a static field
        double s = 0;
        for (int i = 0; i < n; i++) {
            Vec v = new Vec(i, i + 1);
            escapee = v;
            s += v.len();
        }
        return s;
    }

    public static void main(String[] args) {
        double keep = 0;
        for (int i = 0; i < 200; i++) { keep += sumLocal(10_000) + sumEscaping(10_000); }

        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        if (bean instanceof com.sun.management.ThreadMXBean sun
                && sun.isThreadAllocatedMemorySupported()) {
            long a0 = sun.getCurrentThreadAllocatedBytes();
            keep += sumLocal(2_000_000);
            long a1 = sun.getCurrentThreadAllocatedBytes();
            keep += sumEscaping(2_000_000);
            long a2 = sun.getCurrentThreadAllocatedBytes();
            System.out.println("2,000,000 Vec objects written in the source, both times.");
            System.out.println("bytes allocated, non-escaping : " + (a1 - a0));
            System.out.println("bytes allocated, escaping     : " + (a2 - a1));
            System.out.println("The first lot mostly never became heap objects at all.");
        } else {
            System.out.println("this VM does not expose per-thread allocation counters");
        }
        System.out.println("checksum " + (long) keep);
    }
}

Short methods and short-lived objects are exactly what the JIT rewards.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JVM course, and every lesson in it is listed on the JVM contents page.