Skip to content

Commit f5d35bd

Browse files
committed
[GR-51446] Handle unstructured locking to avoid parser bailouts where possible.
PullRequest: graal/17548
2 parents d1ad7c7 + 1c93430 commit f5d35bd

File tree

11 files changed

+350
-54
lines changed

11 files changed

+350
-54
lines changed

compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/core/test/GraphUtilOriginalValueTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public void testUnbalancedMonitors() throws ClassNotFoundException {
170170
Assert.fail("expected a " + BailoutException.class.getName());
171171
} catch (BailoutException e) {
172172
String msg = e.getMessage();
173-
Assert.assertTrue(msg, msg.contains("unbalanced monitors"));
173+
Assert.assertTrue(msg, msg.contains("Locks cannot be merged."));
174174
}
175175
}
176176
}

compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/core/test/UnbalancedMonitorsTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import jdk.graal.compiler.options.OptionValues;
3939
import jdk.graal.compiler.phases.OptimisticOptimizations;
4040
import jdk.vm.ci.code.BailoutException;
41+
import jdk.vm.ci.code.InstalledCode;
4142
import jdk.vm.ci.meta.ResolvedJavaMethod;
4243

4344
/**
@@ -47,6 +48,9 @@
4748
* unstructured locking aren't compiled and fall back to the interpreter. Having the Graal parser
4849
* handle this directly is simplifying for targets of Graal since they don't have to provide a data
4950
* flow that checks this property.
51+
* </p>
52+
* Since [GR-51446], Graal defers some checks to run time, e.g., if it cannot be proven statically
53+
* that an unlocked object matches the object on top of the monitor stack.
5054
*/
5155
public class UnbalancedMonitorsTest extends GraalCompilerTest {
5256
private static final String CLASS_NAME = UnbalancedMonitorsTest.class.getName();
@@ -58,7 +62,10 @@ public class UnbalancedMonitorsTest extends GraalCompilerTest {
5862

5963
@Test
6064
public void runWrongOrder() throws Exception {
61-
checkForBailout("wrongOrder");
65+
ResolvedJavaMethod method = getResolvedJavaMethod(LOADER.findClass(INNER_CLASS_NAME), "wrongOrder");
66+
InstalledCode code = getCode(method);
67+
code.executeVarargs(null, new Object(), new Object());
68+
assertTrue("Deopt expected due to unlocked object not matching top of monitor stack.", !code.isValid());
6269
}
6370

6471
@Test
@@ -93,7 +100,7 @@ private void checkForBailout(String name) throws ClassNotFoundException {
93100
GraphBuilderPhase.Instance graphBuilder = new TestGraphBuilderPhase.Instance(getProviders(), graphBuilderConfig, optimisticOpts, null);
94101
graphBuilder.apply(graph);
95102
} catch (BailoutException e) {
96-
if (e.getMessage().contains("unbalanced monitors") ||
103+
if (e.getMessage().toLowerCase().contains("unstructured locking") ||
97104
// tooFewExits is caught by the FrameStateBuilder
98105
e.getMessage().contains("will underflow the bytecode stack")) {
99106
return;

compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/hotspot/test/GraalOSRTestBase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ public static int[] getBackedgeBCIs(DebugContext debug, ResolvedJavaMethod metho
103103

104104
List<Integer> backedgeBcis = new ArrayList<>();
105105
for (BciBlockMapping.BciBlock block : bciBlockMapping.getBlocks()) {
106-
if (block.getStartBci() != -1) {
106+
// ignore exception entries, as they may never be reached
107+
if (block.getStartBci() != -1 && !block.isExceptionEntry()) {
107108
int bci = block.getEndBci();
108109
for (BciBlockMapping.BciBlock succ : block.getSuccessors()) {
109110
if (succ.getStartBci() != -1) {

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/gen/DebugInfoBuilder.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,6 @@ protected BytecodeFrame computeFrameForState(NodeWithState node, FrameState stat
268268

269269
assert state.bci != BytecodeFrame.AFTER_EXCEPTION_BCI || state.locksSize() == 0 : Assertions.errorMessageContext("node", node, "state", state);
270270

271-
assert !(state.getMethod().isSynchronized() && state.bci != BytecodeFrame.BEFORE_BCI && state.bci != BytecodeFrame.AFTER_BCI && state.bci != BytecodeFrame.AFTER_EXCEPTION_BCI) ||
272-
!state.isValidForDeoptimization() ||
273-
state.locksSize() > 0 : Assertions.errorMessageContext("state", state, "node", node, "bci", state.bci);
274271
assert state.verify();
275272

276273
int numLocals = state.localsSize();

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/java/BciBlockMapping.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,8 @@ private void iterateOverBytecodes(BytecodeStream stream) {
12051205
case LDC:
12061206
case LDC_W:
12071207
case LDC2_W:
1208-
case MONITORENTER: {
1208+
case MONITORENTER:
1209+
case MONITOREXIT: {
12091210
/*
12101211
* All bytecodes that can trigger lazy class initialization via a
12111212
* ClassInitializationPlugin (allocations, static field access) must be listed
@@ -1351,11 +1352,11 @@ private void iterateOverBytecodes(BytecodeStream stream) {
13511352
case FCMPG:
13521353
case DCMPL:
13531354
case DCMPG:
1354-
case MONITOREXIT:
1355-
// All stack manipulation, comparison, conversion and arithmetic operators
1356-
// except for idiv and irem can't throw exceptions so the don't need to connect
1357-
// exception edges. MONITOREXIT can't throw exceptions in the context of
1358-
// compiled code because of the structured locking requirement in the parser.
1355+
/*
1356+
* All stack manipulation, comparison, conversion and arithmetic operators
1357+
* except for idiv and irem can't throw exceptions so the don't need to connect
1358+
* exception edges.
1359+
*/
13591360
break;
13601361

13611362
case WIDE:

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/java/BytecodeParser.java

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -860,22 +860,34 @@ protected void handleReturnMismatch(StructuredGraph g, FrameState fs) {
860860
}
861861
}
862862

863-
private static final class Target {
863+
protected static final class Target {
864864
final FixedNode entry;
865865
final FixedNode originalEntry;
866866
final FrameStateBuilder state;
867867

868-
Target(FixedNode entry, FrameStateBuilder state) {
868+
public Target(FixedNode entry, FrameStateBuilder state) {
869869
this.entry = entry;
870870
this.state = state;
871871
this.originalEntry = null;
872872
}
873873

874-
Target(FixedNode entry, FrameStateBuilder state, FixedNode originalEntry) {
874+
public Target(FixedNode entry, FrameStateBuilder state, FixedNode originalEntry) {
875875
this.entry = entry;
876876
this.state = state;
877877
this.originalEntry = originalEntry;
878878
}
879+
880+
public FixedNode getEntry() {
881+
return entry;
882+
}
883+
884+
public FixedNode getOriginalEntry() {
885+
return originalEntry;
886+
}
887+
888+
public FrameStateBuilder getState() {
889+
return state;
890+
}
879891
}
880892

881893
@SuppressWarnings("serial")
@@ -918,7 +930,7 @@ static boolean containsReturnValue(List<ReturnToCallerData> list, ValueNode valu
918930
private LineNumberTable lnt;
919931
private BitSet emittedLineNumbers;
920932

921-
private ValueNode methodSynchronizedObject;
933+
protected ValueNode methodSynchronizedObject;
922934

923935
private List<ReturnToCallerData> returnDataList;
924936
private ValueNode unwindValue;
@@ -1934,7 +1946,7 @@ protected void genInvokeSpecial(int cpi, int opcode) {
19341946
genInvokeSpecial(target);
19351947
}
19361948

1937-
void genInvokeSpecial(JavaMethod target) {
1949+
protected void genInvokeSpecial(JavaMethod target) {
19381950
if (callTargetIsResolved(target)) {
19391951
assert target != null;
19401952
assert target.getSignature() != null;
@@ -2953,6 +2965,10 @@ private void beforeReturn(ValueNode x, JavaKind kind) {
29532965
*/
29542966
append(new FinalFieldBarrierNode(entryBCI == INVOCATION_ENTRY_BCI ? originalReceiver : null));
29552967
}
2968+
int expectedDepth = frameState.getMethod().isSynchronized() ? 1 : 0;
2969+
if (frameState.lockDepth(false) > expectedDepth) {
2970+
handleUnstructuredLocking("too few monitorexits exiting frame", false);
2971+
}
29562972
synchronizedEpilogue(BytecodeFrame.AFTER_BCI, x, kind);
29572973
}
29582974

@@ -2968,29 +2984,61 @@ protected void genMonitorEnter(ValueNode x, int bci) {
29682984
monitorEnter.setStateAfter(createFrameState(bci, monitorEnter));
29692985
}
29702986

2971-
protected void genMonitorExit(ValueNode x, ValueNode escapedValue, int bci, boolean epilogue) {
2972-
// If a bytecode attempts to pop the last lock in a synchronized method then this method
2973-
// doesn't having properly matching locks so we should bailout. Normally this is detected by
2974-
// the final exit underflowing the lock stack but there is no guarantee that the exit is
2975-
// ever parsed so we should bailout here instead.
2976-
int expectedDepth = frameState.getMethod().isSynchronized() && !epilogue ? 1 : 0;
2977-
if (frameState.lockDepth(false) == expectedDepth) {
2978-
throw bailout("unbalanced monitors: too many exits");
2987+
protected void genMonitorExit(ValueNode x, ValueNode escapedValue, int bci, boolean needsNullCheck) {
2988+
/*
2989+
* This null check ensures Java spec compatibility, where a NullPointerException has
2990+
* precedence over an IllegalMonitorStateException. In the presence of structured locking,
2991+
* this check should fold away, as the non-nullness is already proven by the corresponding
2992+
* monitorenter.
2993+
*/
2994+
ValueNode maybeNullCheckedX = needsNullCheck ? maybeEmitExplicitNullCheck(x) : x;
2995+
/*
2996+
* Graal enforces structured locking. In accordance to Rule 2, it has to throw an
2997+
* IllegalMonitorStateException (or deopt) if the number of monitorexits exceeds the number
2998+
* of monitorenters at any time during method execution.
2999+
*/
3000+
if (frameState.lockDepth(false) == 0) {
3001+
handleUnstructuredLocking("too many monitorexits", false);
3002+
return;
29793003
}
29803004
MonitorIdNode monitorId = frameState.peekMonitorId();
29813005
ValueNode lockedObject = frameState.popLock();
29823006
// if we merged two monitor ids we trust the merging logic checked the correct enter bcis
29833007
if (!monitorId.isMultipleEntry()) {
29843008
ValueNode originalLockedObject = GraphUtil.originalValue(lockedObject, false);
2985-
ValueNode originalX = GraphUtil.originalValue(x, false);
3009+
ValueNode originalX = GraphUtil.originalValue(maybeNullCheckedX, false);
29863010
if (originalLockedObject != originalX) {
2987-
throw bailout(String.format("unbalanced monitors: mismatch at monitorexit, %s != %s", originalLockedObject, originalX));
3011+
// at this point, the lock objects could still be equal, but not visibly to the
3012+
// parser; add a run-time check
3013+
LogicNode eq = append(genObjectEquals(maybeNullCheckedX, lockedObject));
3014+
IfNode ifNode = append(new IfNode(eq, null, null, BranchProbabilityNode.EXTREMELY_FAST_PATH_PROFILE));
3015+
3016+
// lock objects not equal
3017+
ifNode.setFalseSuccessor(graph.add(new BeginNode()));
3018+
lastInstr = ifNode.falseSuccessor();
3019+
FrameStateBuilder oldState = frameState.copy();
3020+
frameState.pushLock(lockedObject, monitorId);
3021+
handleMismatchAtMonitorexit();
3022+
frameState = oldState;
3023+
3024+
// lock objects equal
3025+
ifNode.setTrueSuccessor(graph.add(new BeginNode()));
3026+
lastInstr = ifNode.trueSuccessor();
29883027
}
29893028
}
29903029
MonitorExitNode monitorExit = append(new MonitorExitNode(lockedObject, monitorId, escapedValue));
29913030
monitorExit.setStateAfter(createFrameState(bci, monitorExit));
29923031
}
29933032

3033+
@SuppressWarnings("unused")
3034+
protected void handleUnstructuredLocking(String msg, boolean isDeadEnd) {
3035+
throw bailout("Unstructured locking: " + msg);
3036+
}
3037+
3038+
protected void handleMismatchAtMonitorexit() {
3039+
append(new DeoptimizeNode(DeoptimizationAction.InvalidateRecompile, DeoptimizationReason.TransferToInterpreter));
3040+
}
3041+
29943042
protected void genJsr(int dest) {
29953043
BciBlock successor = currentBlock.getJsrSuccessor();
29963044
assert successor.startBci == dest : successor.startBci + " != " + dest + " @" + bci();
@@ -3191,10 +3239,16 @@ private Target checkUnwind(FixedNode target, BciBlock targetBlock, FrameStateBui
31913239
if (targetBlock != blockMap.getUnwindBlock()) {
31923240
return new Target(target, state);
31933241
}
3194-
FrameStateBuilder newState = state;
3195-
newState = newState.copy();
3242+
FrameStateBuilder newState = state.copy();
31963243
newState.setRethrowException(false);
3197-
if (!method.isSynchronized()) {
3244+
if (!method.isSynchronized() || methodSynchronizedObject == null) {
3245+
/*
3246+
* methodSynchronizedObject==null indicates that the methodSynchronizeObject has been
3247+
* released unexpectedly due to unstructured locking but we are already on the path to
3248+
* the unwind for throwing an IllegalMonitorStateException. Thus, we need to break up an
3249+
* exception loop in the unwind path, which would repeatedly try to release the
3250+
* methodSynchronizedObject via the synchronizedEpilogue.
3251+
*/
31983252
return new Target(target, newState);
31993253
}
32003254
FixedWithNextNode originalLast = lastInstr;
@@ -3224,11 +3278,11 @@ private void setEntryState(BciBlock block, FrameStateBuilder entryState) {
32243278
this.entryStateArray[block.id] = entryState;
32253279
}
32263280

3227-
private void setFirstInstruction(BciBlock block, FixedWithNextNode firstInstruction) {
3281+
protected void setFirstInstruction(BciBlock block, FixedWithNextNode firstInstruction) {
32283282
this.firstInstructionArray[block.id] = firstInstruction;
32293283
}
32303284

3231-
private FixedWithNextNode getFirstInstruction(BciBlock block) {
3285+
protected FixedWithNextNode getFirstInstruction(BciBlock block) {
32323286
return firstInstructionArray[block.id];
32333287
}
32343288

@@ -3272,6 +3326,19 @@ private FixedNode createTarget(BciBlock block, FrameStateBuilder state, boolean
32723326
assert !block.isExceptionEntry() || state.stackSize() == 1 : Assertions.errorMessage(block, state);
32733327

32743328
try (DebugCloseable context = openNodeContext(state, block.startBci)) {
3329+
if (block == blockMap.getUnwindBlock()) {
3330+
int expectedDepth = state.getMethod().isSynchronized() && methodSynchronizedObject != null ? 1 : 0;
3331+
/*
3332+
* methodSynchronizeObject==null indicates that the methodSynchronizeObject has been
3333+
* released unexpectedly but we are already on the path to the unwind for throwing
3334+
* an IllegalMonitorStateException. Thus, we need to break up an exception loop in
3335+
* the unwind path.
3336+
*/
3337+
if (state.lockDepth(false) != expectedDepth) {
3338+
return handleUnstructuredLockingForUnwindTarget("too few monitorexits exiting frame", state);
3339+
}
3340+
}
3341+
32753342
if (getFirstInstruction(block) == null) {
32763343
/*
32773344
* This is the first time we see this block as a branch target. Create and return a
@@ -3294,8 +3361,7 @@ private FixedNode createTarget(BciBlock block, FrameStateBuilder state, boolean
32943361
} else {
32953362
setFirstInstruction(block, graph.add(new BeginNode()));
32963363
}
3297-
Target target = checkUnwind(getFirstInstruction(block), block, state);
3298-
target = checkLoopExit(target, block);
3364+
Target target = checkLoopExit(checkUnwind(getFirstInstruction(block), block, state), block);
32993365
FixedNode result = target.entry;
33003366
FrameStateBuilder currentEntryState = target.state == state ? (canReuseState ? state : state.copy()) : target.state;
33013367
setEntryState(block, currentEntryState);
@@ -3313,7 +3379,7 @@ private FixedNode createTarget(BciBlock block, FrameStateBuilder state, boolean
33133379
*/
33143380
LoopBeginNode loopBegin = (LoopBeginNode) getFirstInstruction(block);
33153381
LoopEndNode loopEnd = graph.add(new LoopEndNode(loopBegin));
3316-
Target target = checkLoopExit(new Target(loopEnd, state.copy()), block);
3382+
Target target = checkUnstructuredLocking(checkLoopExit(new Target(loopEnd, state.copy()), block), block, getEntryState(block));
33173383
FixedNode result = target.entry;
33183384
/*
33193385
* It is guaranteed that a loop header cannot be an ExceptionDispatchBlock. By the
@@ -3324,8 +3390,8 @@ private FixedNode createTarget(BciBlock block, FrameStateBuilder state, boolean
33243390
assert !getEntryState(block).rethrowException();
33253391
target.state.setRethrowException(false);
33263392
getEntryState(block).merge(loopBegin, target.state);
3327-
33283393
debug.log("createTarget %s: merging backward branch to loop header %s, result: %s", block, loopBegin, result);
3394+
33293395
return result;
33303396
}
33313397
assert currentBlock == null || currentBlock.getId() < block.getId() : "must not be backward branch";
@@ -3361,16 +3427,29 @@ private FixedNode createTarget(BciBlock block, FrameStateBuilder state, boolean
33613427

33623428
// The EndNode for the newly merged edge.
33633429
EndNode newEnd = graph.add(new EndNode());
3364-
Target target = checkLoopExit(checkUnwind(newEnd, block, state), block);
3430+
Target target = checkUnstructuredLocking(checkLoopExit(checkUnwind(newEnd, block, state), block), block, getEntryState(block));
33653431
FixedNode result = target.entry;
33663432
getEntryState(block).merge(mergeNode, target.state);
33673433
mergeNode.addForwardEnd(newEnd);
3368-
33693434
debug.log("createTarget %s: merging state, result: %s", block, result);
3435+
33703436
return result;
33713437
}
33723438
}
33733439

3440+
@SuppressWarnings("unused")
3441+
protected FixedNode handleUnstructuredLockingForUnwindTarget(String msg, FrameStateBuilder state) {
3442+
throw bailout("Unstructured locking: " + msg);
3443+
}
3444+
3445+
@SuppressWarnings("unused")
3446+
protected Target checkUnstructuredLocking(Target target, BciBlock targetBlock, FrameStateBuilder mergeState) {
3447+
if (mergeState.areLocksMergeableWith(target.state)) {
3448+
return target;
3449+
}
3450+
throw bailout("Locks cannot be merged. Possibly unstructured locking, which is not supported.");
3451+
}
3452+
33743453
/**
33753454
* Returns a block begin node with the specified state. If the specified probability is 0, the
33763455
* block deoptimizes immediately.
@@ -3429,9 +3508,7 @@ protected void processBlock(BciBlock block) {
34293508
}
34303509

34313510
private void handleUnwindBlock() {
3432-
if (frameState.lockDepth(false) != 0) {
3433-
throw bailout("unbalanced monitors: too few exits exiting frame");
3434-
}
3511+
GraalError.guarantee(frameState.lockDepth(false) == 0, "Unstructured locking: unreleased locks at unwind block!");
34353512
assert !frameState.rethrowException();
34363513
if (parent == null) {
34373514
createUnwind();
@@ -3470,12 +3547,9 @@ private void synchronizedEpilogue(int bci, ValueNode currentReturnValue, JavaKin
34703547
// push the return value on the stack
34713548
frameState.push(currentReturnValueKind, currentReturnValue);
34723549
}
3473-
genMonitorExit(methodSynchronizedObject, currentReturnValue, bci, true);
3550+
genMonitorExit(methodSynchronizedObject, currentReturnValue, bci, false);
34743551
assert !frameState.rethrowException();
34753552
}
3476-
if (frameState.lockDepth(false) != 0) {
3477-
throw bailout("unbalanced monitors: too few exits exiting frame");
3478-
}
34793553
}
34803554
}
34813555

@@ -5673,7 +5747,7 @@ public final void processBytecode(int bci, int opcode) {
56735747
case CHECKCAST : genCheckCast(stream.readCPI()); break;
56745748
case INSTANCEOF : genInstanceOf(stream.readCPI()); break;
56755749
case MONITORENTER : genMonitorEnter(frameState.pop(JavaKind.Object), stream.nextBCI()); break;
5676-
case MONITOREXIT : genMonitorExit(frameState.pop(JavaKind.Object), null, stream.nextBCI(), false); break;
5750+
case MONITOREXIT : genMonitorExit(frameState.pop(JavaKind.Object), null, stream.nextBCI(), true); break;
56775751
case MULTIANEWARRAY : genNewMultiArray(stream.readCPI()); break;
56785752
case IFNULL : genIfNull(Condition.EQ); break;
56795753
case IFNONNULL : genIfNull(Condition.NE); break;

0 commit comments

Comments
 (0)