JetBrainsRuntime icon indicating copy to clipboard operation
JetBrainsRuntime copied to clipboard

DCEVM reloading fails with NoSuchMethodError when changing lambda captures in active stack frame

Open natanfudge opened this issue 9 months ago • 3 comments

Steps to reproduce

  1. 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

Image

  • Add the hotswap-agent-2.0.2.jar file from here into the lib/hotswap directory in the JBR, and rename it into hotswap-agent.jar.
  • You can use the -core hotswap agent, or no hotswap agent at all, and it will crash all the same.
  1. 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();
    }
}
  1. 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.

natanfudge avatar May 07 '25 21:05 natanfudge

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.

natanfudge avatar May 08 '25 09:05 natanfudge

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.

skybber avatar May 08 '25 09:05 skybber

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

natanfudge avatar May 08 '25 11:05 natanfudge