JetBrainsRuntime icon indicating copy to clipboard operation
JetBrainsRuntime copied to clipboard

DCEVM reloading fails with NoSuchMethodError when changing lambda captures of lambda stored in memory and rerunning it

Open natanfudge opened this issue 6 months ago • 2 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:
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)
}
  1. 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
]

natanfudge avatar May 08 '25 12:05 natanfudge

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.

skybber avatar May 09 '25 06:05 skybber

  1. 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.
  2. Why the crash appears

    • Before the reload a lambda captured the variable x, so the compiler produced a helper method subcallAdd$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 throws NoSuchMethodError.
  3. 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.
  4. Work-arounds you can use

    • Create fresh lambdas after the reload – e.g. clear the list and call subcallAdd() again.

Because the behaviour is inherent to how lambdas and invokedynamic work, it can’t be solved automatically.

skybber avatar May 22 '25 18:05 skybber