Skip to content

OpenJ9 JIT produces non-deterministic output from a deterministic fixed-seed program under the default (sampled) compilation strategy #24909

Description

@TemporaryRepos

Java -version output

openjdk version "25.0.4.1" 2026-08-18 LTS
IBM Semeru Runtime Open Edition 25.0.4.15 (build 25.0.4.1+1-LTS)
Eclipse OpenJ9 VM 25.0.4.15 (build 25.0.4.1+1-openj9-0.61.0, JRE 25 Mac OS X aarch64-64-Bit 20260818_330 (JIT enabled, AOT enabled)
OpenJ9   - a1daf225f5
OMR      - ebd02d912
JCL      - cdc2304e39f based on jdk-25.0.4.1+1)

macOS 15, aarch64

Summary of problem

A single-threaded, fully deterministic program (fixed seed, no I/O, no clocks, no hashing in the output path) gives different outputs across runs of the same binary on the same machine when the OpenJ9 JIT is enabled with default options. The reproducer runs the same fixed-seed accumulation logic twice inside one JVM (R1, R2) and prints DUP=same|diff. Under the default sampled compilation strategy, R1 intermittently takes a fresh wrong value on every affected run, while R2 is always correct — 160/160 observed runs across four builds. This localizes the defect to the transition window during the first execution of the loop nest (interpreted → compiled): once the method is fully compiled, results are correct again.

Core shape of the affected loop (JLS 5.1.4 / 15.17.2 int wrap-around semantics):

Random r = new Random(42);
for (int i = 0; i < 500000; i++) {
    x += Math.abs(r.nextInt());
    while (x < 0) x += 1000000000;   // repair int overflow wrap-around
    agg += (x % 97);
}

R1 must equal R2 and must equal the interpreter result (23997686) on every run of every conforming JVM.

Trigger characterization (all on default options unless noted):

  • Structure: the int-overflow wrap-around correction loop is the trigger surface. An otherwise identical program with the correction replaced by manual bit arithmetic is 0/63 runs nondeterministic (negative control). The aggregation style and the RNG are not involved: a hand-written LCG variant still misbehaves (5–20% of runs), ruling out java.util.Random variance.
  • High-rate form: polynomial aggregation (agg = agg*31 + (x%97)) with a while correction raises the trigger rate to 50–75% per run; wrong values are amplified by the polynomial to sign-flipped magnitudes.
  • Scale: 500,000 iterations triggers; 250,000 and below → 0/20 on both 17.0.20 and 25.0.4 (the JIT must take over mid-loop for the window to open).
  • Compilation mode: default sampled escalation triggers; explicit -Xjit:optlevel=cold / -Xjit:optlevel=hot and -Xint are always correct.

Affected version matrix (macOS 15, aarch64):

  • Affected (default options): Semeru 11.0.32, 17.0.20, 21.0.12, 25.0.4 — all four releases intermittently nondeterministic (5–40% of runs for the base shape).
  • Unaffected (controls, same binaries): -Xint; -Xjit:optlevel=cold; -Xjit:optlevel=hot; Temurin 17/21/25 (default and -Xint).

Violated specification: a deterministic Java program (fixed seed, single thread, pure integer arithmetic with JLS-mandated wrap-around) must produce the same result on every execution of the same JVM build. JIT-compiled code is required to be observationally consistent with interpreted code of the same JVM; here the same method produces different results across runs, and even within a single run (DUP=diff, R1 wrong / R2 right).

This issue was identified via differential testing across multiple JVM implementations (Temurin and IBM Semeru/OpenJ9, JDK 11–25).

Steps to reproduce

  1. javac repro.java
  2. java repro — repeat ~10–20 times; intermittently prints DUP=diff with a fresh wrong R1 each time (probability 5–40% per run depending on build; the polynomial variant reaches 50–75%)

Expected result

CHAINL=5
R1=23997686
R2=23997686
DUP=same

on every run, in every execution mode, on every conforming JVM.

Actual result

Example (Semeru 17.0.20, default, 20-run sample — 8 wrong, 8 distinct values; all exit code 0, no exceptions):

run=1  R1=24012322 R2=23997686 DUP=diff
run=2  R1=24000473 R2=23997686 DUP=diff
run=5  R1=23996673 R2=23997686 DUP=diff
run=8  R1=23996515 R2=23997686 DUP=diff
run=11 R1=24005587 R2=23997686 DUP=diff
run=13 R1=23993056 R2=23997686 DUP=diff
run=19 R1=24002947 R2=23997686 DUP=diff
run=20 R1=24002933 R2=23997686 DUP=diff

Reproduction matrix (2 rounds × 20 runs × 4 builds, default options; DUP=diff counts):

Semeru build round 1 round 2
11.0.32 7/20 6/20
17.0.20 8/20 3/20
21.0.12 4/20 5/20
25.0.4 4/20 1/20

Controls (-Xint, -Xjit:optlevel=cold, -Xjit:optlevel=hot, Temurin 17/21/25): 0 wrong in every sample. R2 correct in 160/160 runs across all affected builds.

Source code / Diagnostic files

import java.util.Random;

public class repro {
    static long runOnce() {
        Random r = new Random(42);
        int x = 0;
        long agg = 0;
        for (int i = 0; i < 500000; i++) {
            x += Math.abs(r.nextInt());
            while (x < 0) {
                x += 1000000000; // correct int wrap-around
            }
            agg += (x % 97);
        }
        return agg;
    }

    static void realMain() {
        System.out.println("CHAINL=5");
        long r1 = runOnce();
        long r2 = runOnce();
        System.out.println("R1=" + r1);
        System.out.println("R2=" + r2);
        System.out.println("DUP=" + (r1 == r2 ? "same" : "diff"));
    }

    public static void main(String[] args) {
        try {
            realMain();
        } catch (Throwable t) {
            System.err.println(t.getClass().getName() + ": " + t.getMessage());
            System.exit(1);
        }
    }
}

Supporting variants used for the characterization above (happy to attach if useful): LCG-isolated form (rules out java.util.Random), polynomial high-rate form (50–75% trigger), and the bit-arithmetic negative control (0/63).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions