oracle/graal · error · PermanentBailoutException

unbalanced monitors - monitors do not match

Error message

unbalanced monitors - monitors do not match

What it means

Companion of the locked-object-count check: both predecessors hold the same number of monitors, but at some index the monitor identities cannot be reconciled (different monitorIds and MonitorIdNode.monitorIdentityEquals fails). Even matching counts are not enough — the same locks, identified by their locking objects, must be held at the merge, otherwise Graal permanently bails out.

Source

Thrown at compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/java/FrameStateBuilder.java:518

        }
        for (int i = 0; i < stackSize(); i++) {
            ValueNode x = stack[i];
            ValueNode y = other.stack[i];
            assert x != null;
            assert y != null;
            if (x != y && (x == TWO_SLOT_MARKER || x.isDeleted() || y == TWO_SLOT_MARKER || y.isDeleted() || x.getStackKind() != y.getStackKind())) {
                throw new GraalError(incompatibilityErrorMessage("mismatch in stack types", other));
            }
        }
        if (lockedObjects.length != other.lockedObjects.length) {
            throw new PermanentBailoutException(incompatibilityErrorMessage("unbalanced monitors - locked objects do not match", other));
        }
        for (int i = 0; i < lockedObjects.length; i++) {
            if (monitorIds[i] != other.monitorIds[i]) {
                if (MonitorIdNode.monitorIdentityEquals(monitorIds[i], other.monitorIds[i])) {
                    continue;
                }
                throw new PermanentBailoutException(incompatibilityErrorMessage("unbalanced monitors - monitors do not match", other));
            }
        }
    }

    public boolean areLocksMergeableWith(FrameStateBuilder other) {
        if (lockedObjects.length != other.lockedObjects.length) {
            return false;
        }
        for (int i = 0; i < lockedObjects.length; i++) {
            if (!MonitorIdNode.monitorIdentityEquals(monitorIds[i], other.monitorIds[i])) {
                return false;
            }
        }
        return true;
    }

    public void merge(AbstractMergeNode block, FrameStateBuilder other) {
        checkCompatibleWith(other);

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Strictly verify the class to locate the offending merge and method.
  2. Restructure the code so the same lock object(s) are held on every path reaching the merge (lock before branching, or unlock before the join).
  3. Regenerate the bytecode without the tool transformation that introduced the divergent locks.
Defensive patterns

Strategy: validation

Validate before calling

// ensure the same lock object is held on all paths into a merge;
// strict verification catches mismatched monitor identities early

Prevention

When it happens

Trigger: The per-index loop over lockedObjects at a merge finding monitorIds[i] != other.monitorIds[i] with different underlying lock identity — e.g. two branches each did monitorenter on different objects and then flow together.

Common situations: Generated/obfuscated bytecode locking different objects on different branches before a join; instrumentation inserting conditional monitors; malformed monitorenter sequences that pass naive count checks but lock distinct objects.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/8624ebf293d1f5e9. Report an issue: GitHub.