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
- Verify the class (`-Xverify:all`) to confirm the monitor imbalance and locate the method.
- Fix the bytecode/source so every path through the region enters and exits the same monitors (proper try/finally or synchronized blocks).
- 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
- Always pair monitorenter/monitorexit in a try/finally shape in generated bytecode.
- Use `synchronized` blocks in source instead of manual monitor bytecode.
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
- unbalanced monitors - monitors do not match
- At %s, bci %s: opcode %s (%s) has a stack effect of %s, the
- OSR compilation without OSR entry loop.
- OSR with locks disabled.
- Unbalanced monitor enter-exit in OSR compilation with locks:
AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14).
Data as JSON: /api/errors/cec2f9a92aa32a5f.
Report an issue: GitHub.