Java learning pathSection 7

JIT Compiler Optimizations

Explore all eleven source optimization topics, from inlining and allocation elimination to loop transformations, lock optimizations, and on-stack replacement.

1

Method Inlining

Inlining substitutes a callee's operations into an optimized caller. The source's square(5) example can become a multiplication in the caller, removing the normal call/return boundary and exposing the arithmetic to further optimization.

Inlining is often more valuable for the transformations it unlocks than for call overhead alone: constants can propagate across the former boundary, escape analysis can see more uses, and redundant work may disappear.

This is a conceptual transformation, not rewritten Java source. HotSpot uses size budgets, profiles, and safety checks; large, recursive, or highly polymorphic calls are not guaranteed to inline. More inlining can increase code size and instruction-cache pressure.

Inlining exposes object uses to escape analysis, which can enable scalar replacement and redundant-work elimination
Optimizations can enable one another, but each transformation remains conditional on legality and profitability.
Example
static int square(int x) { return x * x; }
int result = square(5);
// Conceptually after inlining: int result = 5 * 5;
// Further constant folding may produce: int result = 25;
2

Escape Analysis

Escape analysis asks whether an allocation can become accessible outside the region the compiler can analyze or to another thread. Returning a reference, storing it into shared state, or passing it to unanalyzable code can change the answer.

In the source's calculate() example, Point is used only to add its two coordinates. If the compiler proves the allocation does not escape and can represent its state another way, it may remove the allocation and reduce garbage-collection pressure.

Escape analysis is an analysis, not itself a guarantee of allocation elimination. A nonescaping object may still be allocated because of compiler limitations or other constraints. In HotSpot, avoid explaining this as a blanket promise to move every local object onto the stack.

Example
class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}
static int calculate() {
    Point p = new Point(10, 20);
    return p.x + p.y;
}
// p is not returned or stored into shared state.
// The JIT may remove the allocation; source semantics stay unchanged.
3

Scalar Replacement

Scalar replacement represents an object's state as separate values rather than requiring a materialized object for every use. The source's Point with x = 10 and y = 20 can be represented as the two scalar values and, after folding, just the result 30.

The values may live in registers or other compiler-managed locations; calling them local variables is an explanatory model. This can remove allocation work and reduce memory traffic and GC demand.

Object identity, synchronization, exception behavior, and deoptimization requirements must remain correct. The runtime can retain information needed to reconstruct an object if execution returns to a less optimized representation.

Example
// Conceptual original operations:
// Point p = new Point(10, 20);
// return p.x + p.y;

// Conceptual scalar representation:
int x = 10;
int y = 20;
int result = x + y; // Can fold to 30; no materialized Point needed.
4

Loop Unrolling

Loop unrolling places several logical iterations in one loop body. For the document's eight-element sum, a two-way unrolled body adds arr[i] and arr[i + 1] before performing the next increment and loop test.

Fewer loop-control operations and additional scheduling opportunities can improve throughput. Full unrolling of a tiny fixed-size loop may remove the loop control entirely; partial unrolling balances overhead reduction against code growth.

General lengths require a remainder path. The example below preserves a final odd element rather than assuming every array has exactly eight elements. Bounds, exceptions, side effects, and arithmetic semantics constrain legal transformations.

Loop unrolling groups iterations per test, while loop peeling separates a guarded first iteration from the main loop
Unrolling groups work; peeling isolates work. Preserve empty-input and remainder behavior.
Example
long sum = 0;
int i = 0;
for (; i < arr.length - 1; i += 2) {
    sum += arr[i];
    sum += arr[i + 1];
}
if (i < arr.length) {
    sum += arr[i]; // Remainder for an odd length.
}
// arr is an int[]; this illustrates partial unrolling.
5

Loop Peeling

Peeling separates one or a few initial iterations from the main loop. It can isolate a special first-iteration condition or establish facts that simplify later checks.

The source sketches process(0) followed by a loop beginning at 1. That sketch requires a guard: the original loop does nothing for n <= 0, so unconditional process(0) would change behavior.

The goal is a simpler optimizable main loop, not a universal instruction to manually duplicate loop bodies. Whether peeling helps depends on the loop, target hardware, and compilation policy.

Example
// Original: for (int i = 0; i < n; i++) process(i);

// Semantics-preserving conceptual peeled form:
if (n > 0) {
    process(0);
    for (int i = 1; i < n; i++) {
        process(i);
    }
}
6

Dead Code Elimination

Dead code elimination removes computations that cannot affect observable program behavior. If the source's x = 10, y = 20, z = x + y produce a z that is never used, the unused arithmetic can disappear.

A constant-false branch is another example; some obvious cases may already be handled by javac rather than waiting for the JIT. Work with side effects or required exceptions cannot simply be removed because a return value is unused.

This matters for benchmarking: a loop whose result is never consumed may measure optimized-away work. A realistic benchmark must account for both dead-code elimination and constant folding rather than merely timing a source loop.

Example
int x = 10;
int y = 20;
int z = x + y; // Removable if z has no observable use.

// Different: an unused return value does not make I/O removable.
System.out.println("Hello");
7

Constant Folding

Constant folding evaluates expressions whose operands are known at compilation time. The document's 20 * 5 becomes 100 rather than a multiplication repeated on every execution.

Java source constant expressions can be folded by javac. The JIT can also discover constants through runtime specialization and inlining, so not every folded value originates from a literal expression in source.

Folding must retain Java arithmetic semantics, including integer overflow and floating-point rules. The source's 3.14 * 2 example illustrates avoided runtime arithmetic; it should not imply that arbitrary floating-point expressions can be reassociated without regard to rounding.

Example
int x = 20 * 5;     // Can become 100.
double twicePi = 3.14 * 2; // Constant expression.
// The observable values must remain those required by Java.
8

Common Subexpression Elimination (CSE)

If an expression is recomputed and its inputs cannot change, the compiler can reuse its earlier value. The source computes x + y twice, once multiplied by 2 and once by 5; a shared temporary removes the duplicated addition.

The key condition is equivalence at both uses. Intervening writes, aliasing, volatile reads, exceptions, and side effects can invalidate an apparent common expression. Two calls with identical text are not automatically interchangeable.

CSE reduces redundant work and can improve CPU efficiency, but keeping a temporary live can also increase register pressure. The optimizer balances those costs.

Example
// Before: int a = (x + y) * 2; int b = (x + y) * 5;
int temp = x + y;
int a = temp * 2;
int b = temp * 5;
// Valid here when x and y are stable local values.
9

Lock Elimination

When the compiler proves that an object's monitor cannot be shared with another thread, synchronization on that monitor may be unnecessary. The source creates a fresh Object inside test(), synchronizes on it, and never publishes the lock.

Escape analysis can enable removal of that private monitor's locking operations. The example still prints Hello; eliminating the local lock does not eliminate output, nor does it imply that locks internal to PrintStream disappear.

A lock that is local as a variable is not necessarily thread-confined: its referenced object may have escaped elsewhere. The proof concerns the object's accessibility, not the spelling or scope of one variable.

Example
static void test() {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println("Hello");
    }
}
// The fresh, nonescaping monitor may be eliminated.
// The observable print remains.
10

Lock Coarsening

Lock coarsening reduces repeated acquire/release overhead by extending a synchronized region. The source illustrates adjacent work1() and work2() blocks that synchronize on the same lock.

This differs from elimination: coarsening retains a lock but changes the region covered by a compiler-generated lock operation. A transformation is only possible when it preserves the behavior Java requires.

Do not manually merge critical sections indiscriminately. A larger protected region holds a monitor longer, may reduce concurrency, and can alter the opportunity for other threads to interleave. Source-level refactoring and an optimizer's legally justified transformation are not interchangeable guarantees.

Example
// Source sketch:
synchronized (lock) { work1(); }
synchronized (lock) { work2(); }

// Conceptual coarsened region, only when valid and profitable:
// synchronized (lock) {
//     work1();
//     work2();
// }
11

On-Stack Replacement (OSR)

OSR transfers an already-running method or loop into compiled code without restarting the method. A single long-running loop may get hot even though the method containing it is invoked only once.

The document uses a 100-million-iteration sum: execution begins interpreted, profiling notices the hot loop, the compiler creates an OSR entry, and execution can continue with the current index and accumulated sum preserved.

OSR is an execution-transition mechanism rather than an arithmetic simplification like constant folding. Transition points and compilation availability are implementation-dependent; no exact iteration in the example is a guaranteed switch point.

Example
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
    sum += i;
}
System.out.println(sum);
// OSR can preserve the live i and sum values when entering compiled code.
12

Selective Optimization: Summary and Relationships

The source closes by emphasizing selection: HotSpot uses profiling and compilation heuristics to balance optimization cost against expected gains. These examples explain possible transformations, not a contract that all eleven occur in every program.

Several transformations reinforce one another. Inlining can expose a nonescaping allocation, escape analysis can enable scalar replacement and lock elimination, and the resulting scalar expressions may fold or become dead. Profiling and OSR determine when optimized code becomes useful during execution.

Selective Optimization: Summary and Relationships
OptimizationPrimary effectImportant precondition or trade-off
InliningExpose callee operations in callerCode-size budget and valid dispatch assumptions
Escape analysisDetermine allocation/lock accessibilityProof can be limited by analysis boundaries
Scalar replacementRepresent object state as scalarsMust preserve identity and observable semantics
Loop unrollingMore work per loop-control stepRemainders and code growth
Loop peelingSeparate initial iterationsGuard empty input
Dead code eliminationRemove unobservable workPreserve side effects and required exceptions
Constant foldingPrecompute known expressionsPreserve Java arithmetic semantics
CSEReuse equivalent computationInputs must remain valid
Lock eliminationRemove unnecessary monitor operationsProve monitor need not coordinate threads
Lock coarseningReduce acquire/release pairsLegality and longer lock duration
OSREnter compiled code during an invocationPreserve live execution state