DCEVM reloading fails with NoSuchMethodError when changing lambda captures in active stack frame
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:
public class TestHotReload {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(100);
var x = 10;
javaLambda(() -> {
System.out.println(x);
System.out.println("Halo");
});
}
}
private static void javaLambda(Runnable lambda) {
lambda.run();
}
}
- Comment out the line
System.out.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 TestHotReload.lambda$main$0(int)'
at TestHotReload.javaLambda(TestHotReload.java:15)
at TestHotReload.main(TestHotReload.java:7)
- You can write it in Kotlin, 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 System.out.println(x); i.e. without printing x.
Actual Behavior
Program crashes with NoSuchMethodError on reload with a lambda capture change. In Kotlin (and sometimes in Java) these changes are very common, so "Enhanced Class Redefinition" does not work in most cases.
Environment
Using Windows 11, latest JBR Release 21.0.6b631.42. Using Gradle if the matters.
Notes
It doesn't make much sense; It seems like everyone, especially Jetbrains, would be encountering this all the time, but I could not find any mention of this issue anywhere, and I remember encountering this issue all the way back when I tried DCEVM in 2018. Maybe there is some workaround or configuration everyone uses? EDIT: Here, I found some OG issue in DCEVM https://github.com/dcevm/dcevm/issues/174, Barteks being quite right:
With this issue, hotswap becomes less and less relevant with code that actually uses java 8+ features. And it's likely to become even more of a problem with future java features.
Okay so here's one way to solve this: If you use a suspending coroutine instead of sleep, reloading does work correctly, despite that this code seems equivalent to me:
fun main() {
runBlocking {
while (true) {
delay(100)
val x = 2
lambda {
println(x)
}
}
}
}
fun lambda(thing: () -> Unit) {
thing()
}
You can make changes freely in this case.
Currently, DCEVM has a limitation when modifying code that is part of an active stack frame. In your case, javaLambda() is part of the active stack frame of the main() function. When you modify javaLambda(), DCEVM marks the method as deleted, which prevents hot reload from working correctly.
If you extract the lambda into a separate method, the issue is resolved.
object TestHotReload {
@Throws(InterruptedException::class)
@JvmStatic
fun main(args: Array<String>) {
while (true) {
Thread.sleep(100)
val x = 10
subcall(x)
}
}
private fun subcall(x: Int) {
javaLambda {
println(x)
println("Halo1")
}
}
private fun javaLambda(lambda: Runnable) {
lambda.run()
}
}
This is currently a limitation of DCEVM, but it may be fixed in the future.
Thanks @skybber ! That definitely accounts for this issue, but there is another case where this happens, so I'll open a separate issue for it as it's a different problem: #535