JetBrainsRuntime
JetBrainsRuntime copied to clipboard
DCEVM reloading fails with NoSuchMethodError when changing lambda captures of lambda stored in memory and rerunning it
Steps to reproduce
- Follow the instructions from Youtrack. In short:
- Setup an "Enhanced Class Redefinition" launch configuration, i.e. set VM options to
-XX:+AllowEnhancedClassRedefinition -XX:HotswapAgent=fatjar
- Add the
hotswap-agent-2.0.2.jarfile from here into thelib/hotswapdirectory in the JBR, and rename it intohotswap-agent.jar. - You can use the
-corehotswap agent, or no hotswap agent at all, and it will crash all the same.
- Run this code in debug mode in Intellij IDEA:
val lambdas = mutableListOf<Runnable>()
fun main() {
val x = 10
subcallAdd(x)
while (true) {
Thread.sleep(100)
subcallRun()
}
}
private fun subcallRun() {
lambdas.forEach { it.run() }
}
private fun subcallAdd(x: Int) {
val lambda = Runnable {
println(x)
println("Halo1")
}
lambdas.add(lambda)
}
- Comment out the line
println(x);
By doing this you have made the lambda no longer capture x, and the function signature of the lambda changes from (int) to ()
4. Reload changed classes, using a hotkey, or the new integrated Idea popup button.
5. Observe crash:
Exception in thread "main" java.lang.NoSuchMethodError: 'void MainKt.subcallAdd$lambda$1(int)'
at MainKt.subcallRun(Main.kt:20)
at MainKt.main(Main.kt:15)
at MainKt.main(Main.kt)
- You can write it in Java, or add instead of removing capture, and it will crash all the same.
Expected Behavior
The code reloads successfully; The code continues executing without running println(x); i.e. without printing x.
Actual Behavior
Program crashes with NoSuchMethodError on reload with a lambda capture change.
Environment
Using Windows 11, latest JBR Release 21.0.6b631.42. Using Gradle if the matters.
Notes
- Related to #534 but has a different cause
- If you use an anonymous class instead of a lambda it works:
private fun subcallAdd(x: Int) {
// val lambda = Runnable {
// println(x)
// println("Halo1")
// }
val lambda = object : Runnable {
override fun run() {
println(x)
println("Halo1")
}
}
lambdas.add(lambda)
}
- Same goes with Java - lambda doesn't work; anonymous class does work.
Investigation
While the anonymous class creates a new class and adds an instance of it to the list:
public final class io/github/natanfudge/fn/MainKt$subcallAdd$lambda$1 implements java/lang/Runnable
...
public run()V
...
... In `subcallAdd`:
NEW io/github/natanfudge/fn/MainKt$subcallAdd$lambda$1
The lambda generates a method and adds it to the list using invokedynamic:
private final static subcallAdd$lambda$1(I)V
...
... in `subcallAdd`:
INVOKEDYNAMIC run(I)Ljava/lang/Runnable; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
io/github/natanfudge/fn/MainKt.subcallAdd$lambda$1(I)V,
()V
]
Thank you for the great description and the reproducible example! We'll take a look at it and get back to you once we have more information.
-
What really happens when you reload the class
- DCEVM swaps the byte-code of the class on disk.
- It cannot touch the lambda objects that were already created and are still alive in memory.
-
Why the crash appears
- Before the reload a lambda captured the variable
x, so the compiler produced a helper methodsubcallAdd$lambda$1(int). - After the reload the lambda no longer captures anything, so a different helper method is generated:
subcallAdd$lambda$1(). - Existing lambda objects still hold a direct pointer to the old
(int)method. When they run, the VM looks for that method, does not find it, and throwsNoSuchMethodError.
- Before the reload a lambda captured the variable
-
Why DCEVM cannot “fix” it
- The pointer is stored inside a
ConstantCallSite; by design the JVM treats it as immutable. - DCEVM can’t rewrite those call sites or update every live lambda instance.
- This is a limitation of the JVM linkage model, not a bug in DCEVM.
- The pointer is stored inside a
-
Work-arounds you can use
- Create fresh lambdas after the reload – e.g. clear the list and call
subcallAdd()again.
- Create fresh lambdas after the reload – e.g. clear the list and call
Because the behaviour is inherent to how lambdas and invokedynamic work, it can’t be solved automatically.