Search code examples
scalajvmclassloader

Classloader loading a class for None when foreach is used on it


I define custom JFR events like this:

  @Name("xxxx.jfr.PerfScope")
  @Label("PerfScope scope")
  @Category(Array("Java Application"))
  @Description("Scope with a tracked performance")
  @StackTrace(false)
  class PerfScopeEvent(
    name: String
  ) extends jdk.jfr.Event

  val hasJFR = try {
    FlightRecorder.register(classOf[PerfScopeEvent])
    true
  } catch {
    case _: NoClassDefFoundError =>
      // this is expected when running on JDK 8
      false
  }

When using the event the code does this:

    val event = if (hasJFR) {
      Some(new PerfScopeEvent(scopeName))
    } else {
      None
    }

    event.foreach(_.begin())

    // ....

    event.foreach { e =>
      e.end()
      e.commit()
    }

The purpose of the code is to skip the PerfScopeEvent class functionality when running on Java 8, where class jdk.jfr.Event is not available.

I have verified in the debugger the hasJFR is constructed fine, when running on Java 8 it is false because an exception NoClassDefFoundError is thrown while calling FlightRecorder.register(classOf[PerfScopeEvent]).

The trouble is that in the "using the event part" I get an exception as well. The exception is thrown when event.foreach(_.begin()) is executed. This is confusing me.

The exception is:

Exception in thread "main" java.lang.BootstrapMethodError: java.lang.NoClassDefFoundError: jdk/jfr/Event
Caused by: java.lang.NoClassDefFoundError: jdk/jfr/Event
Caused by: java.lang.ClassNotFoundException: jdk.jfr.Event
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 22 more

I can prevent the exception by using if (event.isDefined) event.foreach(_.begin()) instead (and the same for the second use).

What difference does wrapping in the if (event.isDefined) have? Why does classloader load the class jdk/jfr/Event when using foreach directly?

I was able to create a minimal code which still shows the same issue:

import jdk.jfr._

object OptionalJFR  {

  @Name("com.github.ondrejspanel.jfr.PerfScope")
  @Label("PerfScope scope")
  @Category(Array("Java Application"))
  @Description("Scope with a tracked performance")
  @StackTrace(false)
  class PerfScopeEvent(@Label("name") val name: String) extends Event

  def main(args: Array[String]): Unit = {
    val s = (s: PerfScopeEvent) => ()
  }
}

The bytecode generated from this is:

public final class OptionalJFR$ {
  public static final OptionalJFR$ MODULE$;

  public static {};
    Code:
       0: new           #2                  // class OptionalJFR$
       3: dup
       4: invokespecial #22                 // Method "<init>":()V
       7: putstatic     #24                 // Field MODULE$:LOptionalJFR$;
      10: return

  public void main(java.lang.String[]);
    Code:
       0: invokedynamic #48,  0             // InvokeDynamic #0:apply:()Lscala/Function1;
       5: astore_2
       6: return

  public static final void $anonfun$main$1(OptionalJFR$PerfScopeEvent);
    Code:
       0: return

  private OptionalJFR$();
    Code:
       0: aload_0
       1: invokespecial #56                 // Method java/lang/Object."<init>":()V
       4: return

  public static final java.lang.Object $anonfun$main$1$adapted(OptionalJFR$PerfScopeEvent);
    Code:
       0: aload_0
       1: invokestatic  #58                 // Method $anonfun$main$1:(LOptionalJFR$PerfScopeEvent;)V
       4: getstatic     #64                 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
       7: areturn

  private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
    Code:
       0: aload_0
       1: invokedynamic #76,  0             // InvokeDynamic #1:lambdaDeserialize:(Ljava/lang/invoke/SerializedLambda;)Ljava/lang/Object;
       6: areturn
}


Solution

  • With foreach, the lambda (an instance of Function[PerfScopeEvent, Unit] in this case) is constructed, and then foreach is called. In the case of None, foreach doesn't execute the lambda.

    This means that side effects of constructing the lambda will happen, even if the lambda is never executed. Classloading is such a side effect.

    By wrapping the call to foreach in an if, the lambda is not created until after the condition check.

    That said, once you're doing an isDefined check, it's probably worth considering unwrapping the Some and not using map/flatMap/foreach and friends (it will be more performant by not constructing lambdas, though there are valid reasons to maintain an absolute style). Also note that eq None is almost certainly faster than isDefined.