oracle/graal · error · PermanentBailoutException

unbalanced monitors - locked objects do not match

Error message

unbalanced monitors - locked objects do not match

What it means

Thrown by FrameStateBuilder when two control-flow predecessors reaching a merge point hold different numbers of locked monitors (lockedObjects.length differs). Bytecode requires that at every merge point the monitor stack be identical; a mismatch means the method enters/exits monitors unevenly across paths, which the verifier would reject, so Graal permanently bails out.

Source

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

        if (rethrowException != other.rethrowException) {
            throw new GraalError(incompatibilityErrorMessage("mismatch in rethrowException flag", other));
        }

        if (stackSize() != other.stackSize()) {
            throw new GraalError(incompatibilityErrorMessage("mismatch in stack sizes", other));
        }
        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;

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Verify the class (`-Xverify:all`) to confirm the monitor imbalance and locate the method.
  2. Fix the bytecode/source so every path through the region enters and exits the same monitors (proper try/finally or synchronized blocks).
  3. Update or reconfigure the bytecode tool (obfuscator/agent) whose transformation broke monitor pairing.
Defensive patterns

Strategy: validation

Validate before calling

// verify monitor pairing at merge points
// `java -Xverify:all` rejects monitorenter/exit imbalance before Graal ever sees it

Prevention

When it happens

Trigger: FrameStateBuilder merge compatibility check finding this.lockedObjects.length != other.lockedObjects.length at a control-flow merge — e.g. one branch holding a monitor the other does not (monitorenter without matching monitorexit on all paths).

Common situations: Hand-written or generated bytecode with asymmetric synchronized blocks; obfuscators rewriting try/finally monitor exit sequences; broken monitorenter/monitorexit pairing inserted by instrumentation agents; class files that fail strict verification.

Related errors


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