From 029c60ce0e2b6905aeb094f65108b0965f812e1c Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 12:51:36 +0200 Subject: [PATCH 1/9] Actually initialize JFR Handlers class early to avoid deadlock PR #10096 attempted to preload jdk.jfr.events.Handlers via ClassLoader.loadClass() to prevent an AB-BA deadlock between the JFR Utils monitor and the Handlers class-initialization lock (JDK-8371889). However, ClassLoader.loadClass() only loads the class; it does not run the static initializer and does not acquire the class-initialization lock. Initialization is still deferred to first active use, inside the racing window, so the deadlock is not actually prevented. Use Class.forName(name, true, loader) instead, which forces to run on this thread before the JFR code acquires the Utils monitor. Also widen the catch to Throwable, since forcing initialization can surface Errors (ExceptionInInitializerError, NoClassDefFoundError, LinkageError) that the previous catch (Exception) would not have handled. Co-Authored-By: Claude Opus 4.8 --- .../main/java/datadog/trace/bootstrap/Agent.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 249453c58fb..891a001d914 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -953,12 +953,18 @@ private static synchronized void registerDeadlockDetectionEvent() { private static synchronized void registerSmapEntryEvent() { log.debug("Initializing smap entry scraping"); - // Load JFR Handlers class early, if present (it has been moved and renamed in JDK23+). - // This prevents a deadlock. See https://bugs.openjdk.org/browse/JDK-8371889. + // Initialize the JFR Handlers class early, if present (it has been moved and renamed in + // JDK23+). This prevents a deadlock. See https://bugs.openjdk.org/browse/JDK-8371889. + // We must fully initialize the class here (not just load it) so that its static initializer + // runs on this thread, before the JFR code below acquires the jdk.jfr.internal.Utils monitor. + // ClassLoader.loadClass() only loads the class and does not run , so it does not + // prevent the deadlock. Class.forName(..., true, ...) forces initialization. try { - AGENT_CLASSLOADER.loadClass("jdk.jfr.events.Handlers"); - } catch (Exception e) { - // Ignore when the class is not found or anything else goes wrong. + Class.forName("jdk.jfr.events.Handlers", true, AGENT_CLASSLOADER); + } catch (Throwable t) { + // Ignore when the class is not found or anything else goes wrong. Note that we catch + // Throwable rather than Exception, because forcing initialization can surface Errors such + // as ExceptionInInitializerError, NoClassDefFoundError or LinkageError. } try { From acc41c9e1397a2a38108ed78dfbb7c90d30b24c8 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 17:23:52 +0200 Subject: [PATCH 2/9] Address review: cover renamed holder class and init before all JFR registration Two follow-ups from PR review: - The JFR event-holder class was renamed across JDK versions. It is jdk.jfr.events.Handlers only on JDK 17-18; on JDK 19-22 it is jdk.jfr.events.EventConfigurations (same Utils-based eager init, same deadlock); on JDK 23+ the pattern was removed. Targeting only Handlers meant the workaround silently no-op'd (ClassNotFoundException) on JDK 19-22, which are affected. Try both class names. - registerDeadlockDetectionEvent() also registers a JFR event and runs before registerSmapEntryEvent(). Hoist the holder-class initialization into startJmx() ahead of both registrations so it is always fully initialized before any JFR registration acquires the Utils monitor. Extracted the logic into initializeJfrEventHolderClass(). Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/bootstrap/Agent.java | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 891a001d914..c8e75041ba8 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -906,6 +906,7 @@ private static synchronized void startJmx() { } } if (profilingEnabled) { + initializeJfrEventHolderClass(); registerDeadlockDetectionEvent(); registerSmapEntryEvent(); if (PROFILER_INIT_AFTER_JMX != null) { @@ -950,23 +951,48 @@ private static synchronized void registerDeadlockDetectionEvent() { } } - private static synchronized void registerSmapEntryEvent() { - log.debug("Initializing smap entry scraping"); - - // Initialize the JFR Handlers class early, if present (it has been moved and renamed in - // JDK23+). This prevents a deadlock. See https://bugs.openjdk.org/browse/JDK-8371889. - // We must fully initialize the class here (not just load it) so that its static initializer - // runs on this thread, before the JFR code below acquires the jdk.jfr.internal.Utils monitor. - // ClassLoader.loadClass() only loads the class and does not run , so it does not - // prevent the deadlock. Class.forName(..., true, ...) forces initialization. - try { - Class.forName("jdk.jfr.events.Handlers", true, AGENT_CLASSLOADER); - } catch (Throwable t) { - // Ignore when the class is not found or anything else goes wrong. Note that we catch - // Throwable rather than Exception, because forcing initialization can surface Errors such - // as ExceptionInInitializerError, NoClassDefFoundError or LinkageError. + /** + * Force-initializes the JDK's JFR event-holder class before any JFR event is registered, to avoid + * an ABBA deadlock between {@code jdk.jfr.internal.Utils}'s monitor and this class's + * initialization lock. See JDK-8371889. + * + *

The holder class eagerly initializes the JDK's built-in event handlers through {@code + * jdk.jfr.internal.Utils}. If one thread is running its {@code } (holding the class-init + * lock, then wanting the {@code Utils} monitor) while another thread holds the {@code Utils} + * monitor and tries to trigger that same {@code }, the two deadlock. Fully initializing + * the class here, on this thread, before any JFR registration runs, ensures it is already + * initialized when the {@code Utils} monitor is later taken, so the cycle cannot form. + * + *

The class has been renamed across JDK versions, so we try each known name in turn: + * + *

    + *
  • JDK 17-18: {@code jdk.jfr.events.Handlers} + *
  • JDK 19-22: {@code jdk.jfr.events.EventConfigurations} + *
  • JDK 23+: the eager-initialization pattern was removed, so the deadlock cannot occur and + * neither class is present. + *
+ * + *

{@link Class#forName(String, boolean, ClassLoader)} with {@code initialize=true} is required + * here: {@link ClassLoader#loadClass(String)} only loads the class and does not run its {@code + * }, so it would not acquire the initialization lock early and would not prevent the + * deadlock. + */ + private static void initializeJfrEventHolderClass() { + for (final String className : + new String[] {"jdk.jfr.events.Handlers", "jdk.jfr.events.EventConfigurations"}) { + try { + Class.forName(className, true, AGENT_CLASSLOADER); + return; // at most one of these exists on any given JDK + } catch (final Throwable ignored) { + // Not present on this JDK (renamed/removed) or failed to initialize; try the next name. + // We catch Throwable rather than Exception because forcing initialization can surface + // Errors such as ExceptionInInitializerError, NoClassDefFoundError or LinkageError. + } } + } + private static synchronized void registerSmapEntryEvent() { + log.debug("Initializing smap entry scraping"); try { final Class smapFactoryClass = AGENT_CLASSLOADER.loadClass( From a95e2c8e899c24ee3d7f3099e6ecb7975da5efd4 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 18:15:33 +0200 Subject: [PATCH 3/9] Register JDK JFR events before forcing holder init, to avoid null handlers Forcing the holder class's (jdk.jfr.events.Handlers on JDK 17-18, EventConfigurations on 19-22) before the JDK's built-in events are registered caches null into its static-final handler fields permanently, silently disabling the built-in socket/file/exception JFR events. Empirically verified on JDK 17.0.17 and 21.0.9 (7/7 fields null when the holder is initialized before FlightRecorder; 0/7 when after). In the default profiling.start-force-first=false config the profiler defers FlightRecorder initialization until after startJmx(), so the previous placement would poison the handlers. Force FlightRecorder init (which runs JDKEvents.initialize()) before the holder , mirroring the JDK's own JDK-8371889 fix which initializes the holder only after JDKEvents.initialize(). If FlightRecorder init fails there is nothing to protect, so the holder init is skipped. Collapsed the handling into a single catch(Throwable) with a narrow inner ClassNotFoundException for the Handlers -> EventConfigurations version fallback. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/bootstrap/Agent.java | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index c8e75041ba8..467b6483140 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -952,42 +952,58 @@ private static synchronized void registerDeadlockDetectionEvent() { } /** - * Force-initializes the JDK's JFR event-holder class before any JFR event is registered, to avoid - * an ABBA deadlock between {@code jdk.jfr.internal.Utils}'s monitor and this class's - * initialization lock. See JDK-8371889. + * Force-initializes the JDK's JFR event-holder class early to avoid an ABBA deadlock between + * {@code jdk.jfr.internal.Utils}'s monitor and the holder class's initialization lock. See JDK-8371889. * - *

The holder class eagerly initializes the JDK's built-in event handlers through {@code - * jdk.jfr.internal.Utils}. If one thread is running its {@code } (holding the class-init - * lock, then wanting the {@code Utils} monitor) while another thread holds the {@code Utils} - * monitor and tries to trigger that same {@code }, the two deadlock. Fully initializing - * the class here, on this thread, before any JFR registration runs, ensures it is already - * initialized when the {@code Utils} monitor is later taken, so the cycle cannot form. + *

The holder's {@code } looks up the JDK's built-in event handlers through {@code + * jdk.jfr.internal.Utils} (taking its monitor). The deadlock forms when one thread holds the + * {@code Utils} monitor and wants the holder's class-init lock, while another thread holds the + * class-init lock (running {@code }) and wants the {@code Utils} monitor. Running the + * {@code } here, on this thread, before we register our own JFR events (which is what + * takes the {@code Utils} monitor), means the holder is already initialized by then, so the cycle + * cannot form. * - *

The class has been renamed across JDK versions, so we try each known name in turn: + *

Ordering matters twice over: * *

    - *
  • JDK 17-18: {@code jdk.jfr.events.Handlers} - *
  • JDK 19-22: {@code jdk.jfr.events.EventConfigurations} - *
  • JDK 23+: the eager-initialization pattern was removed, so the deadlock cannot occur and - * neither class is present. + *
  • We force {@code FlightRecorder} initialization first, which registers the JDK's built-in + * events (via {@code JDKEvents.initialize()}). The holder's fields are {@code static final} + * and are populated from {@code Utils} at {@code } time; running {@code } + * before the events are registered would cache {@code null} into those fields permanently + * and silently disable the built-in socket/file/exception JFR events (verified on JDK + * 17.0.17 and 21.0.9). This mirrors the JDK's own fix, which initializes the holder only + * after {@code JDKEvents.initialize()}. If {@code FlightRecorder} init fails, JFR is + * unavailable and there is nothing to protect against, so we skip the holder init entirely. + *
  • {@link Class#forName(String, boolean, ClassLoader)} with {@code initialize=true} is + * required: {@link ClassLoader#loadClass(String)} would only load the class without running + * {@code }, so it would neither take the class-init lock early nor prevent the + * deadlock. *
* - *

{@link Class#forName(String, boolean, ClassLoader)} with {@code initialize=true} is required - * here: {@link ClassLoader#loadClass(String)} only loads the class and does not run its {@code - * }, so it would not acquire the initialization lock early and would not prevent the - * deadlock. + *

The holder class was renamed across JDK versions: {@code jdk.jfr.events.Handlers} on JDK + * 17-18, {@code jdk.jfr.events.EventConfigurations} on JDK 19-22. JDK 23+ removed the eager-init + * pattern (and the JDK-8371889 fix itself was backported to 21.0.11), so on those versions there + * is nothing to initialize and the {@code forName} calls simply fail and are ignored. */ private static void initializeJfrEventHolderClass() { - for (final String className : - new String[] {"jdk.jfr.events.Handlers", "jdk.jfr.events.EventConfigurations"}) { + try { + // Register the JDK's built-in JFR events first, so the holder's below sees non-null + // handlers instead of caching null. + Class.forName("jdk.jfr.FlightRecorder", true, AGENT_CLASSLOADER) + .getMethod("getFlightRecorder") + .invoke(null); + // Force the holder's . The class name depends on the JDK version. try { - Class.forName(className, true, AGENT_CLASSLOADER); - return; // at most one of these exists on any given JDK - } catch (final Throwable ignored) { - // Not present on this JDK (renamed/removed) or failed to initialize; try the next name. - // We catch Throwable rather than Exception because forcing initialization can surface - // Errors such as ExceptionInInitializerError, NoClassDefFoundError or LinkageError. + Class.forName("jdk.jfr.events.Handlers", true, AGENT_CLASSLOADER); // JDK 17-18 + } catch (final ClassNotFoundException notJdk17Or18) { + Class.forName("jdk.jfr.events.EventConfigurations", true, AGENT_CLASSLOADER); // JDK 19-22 } + } catch (final Throwable ignored) { + // JFR unavailable/disabled, holder renamed or removed (JDK 23+), or initialization failed: in + // every case there is nothing (left) to do. We catch Throwable rather than Exception because + // forcing initialization can surface Errors such as ExceptionInInitializerError, + // NoClassDefFoundError or LinkageError. } } From 7afd0e443aafe14df3ffe1544b137ed57abab2f6 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 21:00:36 +0200 Subject: [PATCH 4/9] Add regression test for JFR handler null-poisoning Adds JfrEventHolderInitForkedTest, which runs the production initialization path (Agent.initializeJfrEventHolderClass) in a fresh JVM and asserts the JDK's built-in JFR event-holder fields are not poisoned to null. This guards the ordering invariant that FlightRecorder must be initialized (registering the built-in events) before the holder class's runs; without it the static-final handler fields cache null permanently and silently disable socket/file/exception JFR events. Verified: passes on JDK 17 and 21 with the correct ordering, and fails when the ordering is reversed. Auto-skips on JDKs without the holder class (JDK 8 and 23+). Behavior underpinning the test was reproduced standalone on JDK 17.0.17 and 21.0.9 (7/7 fields null when initialized too early, 0/7 when after FlightRecorder). To make the logic testable, initializeJfrEventHolderClass() takes the ClassLoader as an argument (the production no-arg overload passes AGENT_CLASSLOADER). build.gradle adds --add-opens jdk.jfr/jdk.jfr.events=ALL-UNNAMED so the test can read the holder fields reflectively. Co-Authored-By: Claude Opus 4.8 --- dd-java-agent/agent-bootstrap/build.gradle | 5 ++ .../java/datadog/trace/bootstrap/Agent.java | 13 ++- .../JfrEventHolderInitForkedTest.java | 82 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle index c554039afa5..398c9ad1351 100644 --- a/dd-java-agent/agent-bootstrap/build.gradle +++ b/dd-java-agent/agent-bootstrap/build.gradle @@ -72,4 +72,9 @@ tasks.withType(Test).configureEach { JavaVersion.VERSION_16, ['--add-opens', 'java.base/java.net=ALL-UNNAMED'] // for HostNameResolverForkedTest ) + conditionalJvmArgs( + it, + JavaVersion.VERSION_11, + ['--add-opens', 'jdk.jfr/jdk.jfr.events=ALL-UNNAMED'] // for JfrEventHolderInitForkedTest + ) } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 467b6483140..b072440d9b8 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -987,17 +987,24 @@ private static synchronized void registerDeadlockDetectionEvent() { * is nothing to initialize and the {@code forName} calls simply fail and are ignored. */ private static void initializeJfrEventHolderClass() { + initializeJfrEventHolderClass(AGENT_CLASSLOADER); + } + + // Visible for testing; see JfrEventHolderInitForkedTest. + static void initializeJfrEventHolderClass(final ClassLoader loader) { try { // Register the JDK's built-in JFR events first, so the holder's below sees non-null // handlers instead of caching null. - Class.forName("jdk.jfr.FlightRecorder", true, AGENT_CLASSLOADER) + // Register the JDK's built-in JFR events first, so the holder's below sees non-null + // handlers instead of caching null. + Class.forName("jdk.jfr.FlightRecorder", true, loader) .getMethod("getFlightRecorder") .invoke(null); // Force the holder's . The class name depends on the JDK version. try { - Class.forName("jdk.jfr.events.Handlers", true, AGENT_CLASSLOADER); // JDK 17-18 + Class.forName("jdk.jfr.events.Handlers", true, loader); // JDK 17-18 } catch (final ClassNotFoundException notJdk17Or18) { - Class.forName("jdk.jfr.events.EventConfigurations", true, AGENT_CLASSLOADER); // JDK 19-22 + Class.forName("jdk.jfr.events.EventConfigurations", true, loader); // JDK 19-22 } } catch (final Throwable ignored) { // JFR unavailable/disabled, holder renamed or removed (JDK 23+), or initialization failed: in diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java new file mode 100644 index 00000000000..9c870c28e47 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java @@ -0,0 +1,82 @@ +package datadog.trace.bootstrap; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Regression test for the JFR startup-deadlock workaround in {@link Agent}. + * + *

{@link Agent#initializeJfrEventHolderClass(ClassLoader)} force-initializes the JDK's JFR + * event-holder class ({@code jdk.jfr.events.Handlers} on JDK 17-18, {@code + * jdk.jfr.events.EventConfigurations} on JDK 19-22) to avoid an ABBA deadlock. Its static-final + * handler fields are populated from {@code jdk.jfr.internal.Utils} at {@code } time and + * stay {@code null} forever if the class is initialized before the JDK's built-in events are + * registered (i.e. before {@code FlightRecorder} initialization). A null-poisoned holder silently + * disables the built-in socket/file/exception JFR events. + * + *

This test runs the production initialization path and asserts none of the handler fields were + * poisoned. It is a {@code ForkedTest} because JFR/class initialization happens once per JVM, so + * the ordering under test is only exercised in a fresh JVM that has not yet touched JFR. + * + *

Requires {@code --add-opens jdk.jfr/jdk.jfr.events=ALL-UNNAMED} (configured in build.gradle) + * to read the holder's fields reflectively. Automatically skipped on JDKs without the holder class + * (JDK 8, and JDK 23+ where the eager-init pattern was removed). + */ +public class JfrEventHolderInitForkedTest { + + private static final String[] HOLDER_CLASS_NAMES = { + "jdk.jfr.events.Handlers", // JDK 17-18 + "jdk.jfr.events.EventConfigurations" // JDK 19-22 + }; + + @Test + public void productionInitOrderingDoesNotPoisonHandlers() throws Exception { + final ClassLoader loader = getClass().getClassLoader(); + + // Find the holder class for this JDK without initializing it; skip if none exists (JDK 8, 23+). + final String holderName = findHolderClass(loader); + assumeTrue(holderName != null, "No JFR event-holder class on this JDK; nothing to test"); + + // Exercise the exact production path: FlightRecorder init first, then holder . + Agent.initializeJfrEventHolderClass(loader); + + // The holder is now initialized; read its static handler fields and check none are null. + final Class holder = Class.forName(holderName, false, loader); + final List nullFields = new ArrayList<>(); + int handlerFields = 0; + for (final Field field : holder.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) || field.getType().isPrimitive()) { + continue; + } + handlerFields++; + field.setAccessible(true); + if (field.get(null) == null) { + nullFields.add(field.getName()); + } + } + + assertTrue(handlerFields > 0, "expected " + holderName + " to declare handler fields"); + assertTrue( + nullFields.isEmpty(), + "JFR handler fields were poisoned to null (holder initialized before FlightRecorder): " + + nullFields); + } + + private static String findHolderClass(final ClassLoader loader) { + for (final String name : HOLDER_CLASS_NAMES) { + try { + Class.forName(name, false, loader); // load only, do not initialize + return name; + } catch (final Throwable ignored) { + // not present on this JDK; try the next name + } + } + return null; + } +} From 2f5e1dd970222c6487964a57b0bf73aa1c3f46f0 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 21:13:18 +0200 Subject: [PATCH 5/9] Route JFR event registration through a single init-first choke point Both registerDeadlockDetectionEvent() and registerSmapEntryEvent() shared the same reflective "invoke Factory.registerEvents()" pattern. Extract it into registerJfrEvents(), which force-initializes the JDK JFR event-holder class before registering anything. Routing all startup JFR registration through this one method enforces the ordering invariant structurally, so a future event registration cannot reintroduce the JDK-8371889 deadlock by running before the holder is initialized. The explicit pre-init call in startJmx() is now redundant and removed. Also unifies exception handling: NoClassDefFoundError/ClassNotFoundException/ UnsupportedClassVersionError -> "not supported" (debug); any other Throwable -> error. This is slightly more robust for the smap path, which previously caught only Exception and would have let an Error (e.g. NoClassDefFoundError) propagate out of startup. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/bootstrap/Agent.java | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index b072440d9b8..89b1528be73 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -906,7 +906,8 @@ private static synchronized void startJmx() { } } if (profilingEnabled) { - initializeJfrEventHolderClass(); + // Both of these register JFR events through registerJfrEvents(), which force-initializes the + // JDK's JFR event-holder class first (see initializeJfrEventHolderClass) to avoid a deadlock. registerDeadlockDetectionEvent(); registerSmapEntryEvent(); if (PROFILER_INIT_AFTER_JMX != null) { @@ -936,19 +937,9 @@ When getJmxStartDelay() is set to 0 we will attempt to initialize the JMX subsys private static synchronized void registerDeadlockDetectionEvent() { log.debug("Initializing JMX thread deadlock detector"); - try { - final Class deadlockFactoryClass = - AGENT_CLASSLOADER.loadClass( - "com.datadog.profiling.controller.openjdk.events.DeadlockEventFactory"); - final Method registerMethod = deadlockFactoryClass.getMethod("registerEvents"); - registerMethod.invoke(null); - } catch (final NoClassDefFoundError - | ClassNotFoundException - | UnsupportedClassVersionError ignored) { - log.debug("JMX deadlock detection not supported"); - } catch (final Throwable ex) { - log.error("Unable to initialize JMX thread deadlock detector", ex); - } + registerJfrEvents( + "com.datadog.profiling.controller.openjdk.events.DeadlockEventFactory", + "JMX thread deadlock detection"); } /** @@ -1016,14 +1007,38 @@ static void initializeJfrEventHolderClass(final ClassLoader loader) { private static synchronized void registerSmapEntryEvent() { log.debug("Initializing smap entry scraping"); + registerJfrEvents( + "com.datadog.profiling.controller.openjdk.events.SmapEntryFactory", "smap entry scraping"); + } + + /** + * Registers a profiling JFR event factory's events, after force-initializing the JDK's JFR + * event-holder class (see {@link #initializeJfrEventHolderClass(ClassLoader)}). + * + *

All JFR event registration during agent startup must go through this + * method. Registering a JFR event triggers the holder class's initialization while + * holding the {@code jdk.jfr.internal.Utils} monitor; unless the holder has already been fully + * initialized, that can deadlock (JDK-8371889). Routing every registration through here + * guarantees the holder is initialized first, so future event registrations cannot reintroduce + * the deadlock by running before it. + * + * @param factoryClassName fully-qualified name of the event factory with a static {@code + * registerEvents()} method + * @param description human-readable name used in log messages + */ + private static void registerJfrEvents(final String factoryClassName, final String description) { + // Enforce the ordering invariant: the holder must be initialized before any JFR event is + // registered. Idempotent and cheap once done, so it is safe to call before every registration. + initializeJfrEventHolderClass(); try { - final Class smapFactoryClass = - AGENT_CLASSLOADER.loadClass( - "com.datadog.profiling.controller.openjdk.events.SmapEntryFactory"); - final Method registerMethod = smapFactoryClass.getMethod("registerEvents"); - registerMethod.invoke(null); - } catch (final Exception ignored) { - log.debug("Smap entry scraping not supported"); + final Class factoryClass = AGENT_CLASSLOADER.loadClass(factoryClassName); + factoryClass.getMethod("registerEvents").invoke(null); + } catch (final NoClassDefFoundError + | ClassNotFoundException + | UnsupportedClassVersionError ignored) { + log.debug("{} not supported", description); + } catch (final Throwable ex) { + log.error("Unable to initialize {}", description, ex); } } From 21c98b39f99a0f44d24b35e7377e9506180217be Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 21:47:50 +0200 Subject: [PATCH 6/9] Remove duplicated comment in initializeJfrEventHolderClass Co-Authored-By: Claude Opus 4.8 --- .../src/main/java/datadog/trace/bootstrap/Agent.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 89b1528be73..77b3721334a 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -984,8 +984,6 @@ private static void initializeJfrEventHolderClass() { // Visible for testing; see JfrEventHolderInitForkedTest. static void initializeJfrEventHolderClass(final ClassLoader loader) { try { - // Register the JDK's built-in JFR events first, so the holder's below sees non-null - // handlers instead of caching null. // Register the JDK's built-in JFR events first, so the holder's below sees non-null // handlers instead of caching null. Class.forName("jdk.jfr.FlightRecorder", true, loader) From a8279bbad737e8d34a58eae04e13a4bb117f7b23 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 15 Jul 2026 22:22:11 +0200 Subject: [PATCH 7/9] Address pre-review: docs, version accuracy, log wording, test caveats - Correct the Handlers version range to JDK 15-18 (it is absent on 11-14), and note that JDK 11 LTS and earlier predate the holder class, so they are unaffected. - Move the explanatory Javadoc onto the initializeJfrEventHolderClass implementation (it was on the no-arg wrapper, while @link targets and the real logic are on the ClassLoader overload). - Reference the SCP-1278 deadlock mechanism (smap registration vs. a concurrent JMXFetch ByteBuddy transform) and note the event registration would trigger FlightRecorder init anyway; we only order it first. - Capitalize the smap log description so "Smap entry scraping not supported" reads correctly. - Document the regression test's scope: it checks the ordering invariant (non-null handlers), not end-to-end event emission, and assumes a fresh forked JVM that has not started JFR. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/bootstrap/Agent.java | 46 +++++++++++-------- .../JfrEventHolderInitForkedTest.java | 20 ++++++-- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 77b3721334a..21d299257f8 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -942,18 +942,25 @@ private static synchronized void registerDeadlockDetectionEvent() { "JMX thread deadlock detection"); } + private static void initializeJfrEventHolderClass() { + initializeJfrEventHolderClass(AGENT_CLASSLOADER); + } + /** * Force-initializes the JDK's JFR event-holder class early to avoid an ABBA deadlock between * {@code jdk.jfr.internal.Utils}'s monitor and the holder class's initialization lock. See JDK-8371889. + * href="https://bugs.openjdk.org/browse/JDK-8371889">JDK-8371889 and the SCP-1278 thread + * dump. * *

The holder's {@code } looks up the JDK's built-in event handlers through {@code - * jdk.jfr.internal.Utils} (taking its monitor). The deadlock forms when one thread holds the - * {@code Utils} monitor and wants the holder's class-init lock, while another thread holds the - * class-init lock (running {@code }) and wants the {@code Utils} monitor. Running the - * {@code } here, on this thread, before we register our own JFR events (which is what - * takes the {@code Utils} monitor), means the holder is already initialized by then, so the cycle - * cannot form. + * jdk.jfr.internal.Utils} (taking its monitor); conversely, JFR event registration initializes + * the holder while holding that same monitor. The deadlock forms when one thread holds the {@code + * Utils} monitor and wants the holder's class-init lock, while another thread holds the + * class-init lock (running {@code }) and wants the {@code Utils} monitor. In SCP-1278 + * this was the profiler's smap-event registration (holding {@code Utils}) against a concurrent + * JMXFetch ByteBuddy transform that had triggered the holder {@code }. Running the {@code + * } here, on this thread, before we register our own JFR events, means the holder is + * already initialized by then, so the cycle cannot form. * *

Ordering matters twice over: * @@ -964,8 +971,10 @@ private static synchronized void registerDeadlockDetectionEvent() { * before the events are registered would cache {@code null} into those fields permanently * and silently disable the built-in socket/file/exception JFR events (verified on JDK * 17.0.17 and 21.0.9). This mirrors the JDK's own fix, which initializes the holder only - * after {@code JDKEvents.initialize()}. If {@code FlightRecorder} init fails, JFR is - * unavailable and there is nothing to protect against, so we skip the holder init entirely. + * after {@code JDKEvents.initialize()}. The event registration below would trigger the same + * {@code FlightRecorder} initialization anyway; we merely order it ahead of the holder + * {@code }. If {@code FlightRecorder} init fails, JFR is unavailable and there is + * nothing to protect against, so we skip the holder init entirely. *

  • {@link Class#forName(String, boolean, ClassLoader)} with {@code initialize=true} is * required: {@link ClassLoader#loadClass(String)} would only load the class without running * {@code }, so it would neither take the class-init lock early nor prevent the @@ -973,15 +982,14 @@ private static synchronized void registerDeadlockDetectionEvent() { * * *

    The holder class was renamed across JDK versions: {@code jdk.jfr.events.Handlers} on JDK - * 17-18, {@code jdk.jfr.events.EventConfigurations} on JDK 19-22. JDK 23+ removed the eager-init - * pattern (and the JDK-8371889 fix itself was backported to 21.0.11), so on those versions there - * is nothing to initialize and the {@code forName} calls simply fail and are ignored. + * 15-18, {@code jdk.jfr.events.EventConfigurations} on JDK 19-22. Earlier JDKs (including 11 LTS) + * predate the holder, JDK 23+ removed the eager-init pattern, and the JDK-8371889 fix was + * backported to 21.0.11 -- on all of those there is nothing to initialize and the {@code forName} + * calls simply fail and are ignored. + * + * @param loader class loader used to resolve the JFR classes (package-private for testing; see + * JfrEventHolderInitForkedTest) */ - private static void initializeJfrEventHolderClass() { - initializeJfrEventHolderClass(AGENT_CLASSLOADER); - } - - // Visible for testing; see JfrEventHolderInitForkedTest. static void initializeJfrEventHolderClass(final ClassLoader loader) { try { // Register the JDK's built-in JFR events first, so the holder's below sees non-null @@ -991,7 +999,7 @@ static void initializeJfrEventHolderClass(final ClassLoader loader) { .invoke(null); // Force the holder's . The class name depends on the JDK version. try { - Class.forName("jdk.jfr.events.Handlers", true, loader); // JDK 17-18 + Class.forName("jdk.jfr.events.Handlers", true, loader); // JDK 15-18 } catch (final ClassNotFoundException notJdk17Or18) { Class.forName("jdk.jfr.events.EventConfigurations", true, loader); // JDK 19-22 } @@ -1006,7 +1014,7 @@ static void initializeJfrEventHolderClass(final ClassLoader loader) { private static synchronized void registerSmapEntryEvent() { log.debug("Initializing smap entry scraping"); registerJfrEvents( - "com.datadog.profiling.controller.openjdk.events.SmapEntryFactory", "smap entry scraping"); + "com.datadog.profiling.controller.openjdk.events.SmapEntryFactory", "Smap entry scraping"); } /** diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java index 9c870c28e47..bd7cce7c551 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java @@ -13,7 +13,7 @@ * Regression test for the JFR startup-deadlock workaround in {@link Agent}. * *

    {@link Agent#initializeJfrEventHolderClass(ClassLoader)} force-initializes the JDK's JFR - * event-holder class ({@code jdk.jfr.events.Handlers} on JDK 17-18, {@code + * event-holder class ({@code jdk.jfr.events.Handlers} on JDK 15-18, {@code * jdk.jfr.events.EventConfigurations} on JDK 19-22) to avoid an ABBA deadlock. Its static-final * handler fields are populated from {@code jdk.jfr.internal.Utils} at {@code } time and * stay {@code null} forever if the class is initialized before the JDK's built-in events are @@ -24,14 +24,28 @@ * poisoned. It is a {@code ForkedTest} because JFR/class initialization happens once per JVM, so * the ordering under test is only exercised in a fresh JVM that has not yet touched JFR. * + *

    Scope and limitations: + * + *

      + *
    • It verifies the ordering invariant (non-null handler fields), not end-to-end event + * emission. That keeps it fast and non-flaky; the deadlock itself is timing-dependent and is + * defended structurally (see {@code Agent#registerJfrEvents}) rather than reproduced here. + *
    • It assumes the forked JVM has not initialized JFR before the test runs. If the test JVM is + * ever launched with JFR already started (e.g. {@code -XX:StartFlightRecording} or an + * attached JFR profiler), the holder would already hold valid handlers and the test would + * pass without exercising the ordering. In the standard forked test JVM nothing touches JFR + * first, so the test is meaningful (confirmed: it fails if the FlightRecorder-before-holder + * ordering is reversed). + *
    + * *

    Requires {@code --add-opens jdk.jfr/jdk.jfr.events=ALL-UNNAMED} (configured in build.gradle) * to read the holder's fields reflectively. Automatically skipped on JDKs without the holder class - * (JDK 8, and JDK 23+ where the eager-init pattern was removed). + * (JDK 8 and earlier, and JDK 23+ where the eager-init pattern was removed). */ public class JfrEventHolderInitForkedTest { private static final String[] HOLDER_CLASS_NAMES = { - "jdk.jfr.events.Handlers", // JDK 17-18 + "jdk.jfr.events.Handlers", // JDK 15-18 "jdk.jfr.events.EventConfigurations" // JDK 19-22 }; From 477494a759134de42ad62237952daee4bc9b64a4 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Thu, 16 Jul 2026 11:36:55 +0200 Subject: [PATCH 8/9] Select JFR holder class by version instead of catching CNFE Per review, replace the exception-driven "try Handlers, catch ClassNotFoundException, try EventConfigurations" control flow with an explicit version check. New jfrEventHolderClassName() maps the running JDK to its holder class (Handlers on 15-18, EventConfigurations on 19-22, null otherwise) using JavaVirtualMachine.isJavaVersionAtLeast(), and is the single source of truth shared by the production init path and the regression test (which now uses it to decide the target class and skip JDKs without a holder). The remaining try/catch(Throwable) is genuine error handling (JFR disabled / init failure), not control flow. Co-Authored-By: Claude Opus 4.8 --- .../java/datadog/trace/bootstrap/Agent.java | 52 +++++++++++++------ .../JfrEventHolderInitForkedTest.java | 22 ++------ 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 21d299257f8..0d1f27cb07a 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -981,34 +981,56 @@ private static void initializeJfrEventHolderClass() { * deadlock. * * - *

    The holder class was renamed across JDK versions: {@code jdk.jfr.events.Handlers} on JDK - * 15-18, {@code jdk.jfr.events.EventConfigurations} on JDK 19-22. Earlier JDKs (including 11 LTS) - * predate the holder, JDK 23+ removed the eager-init pattern, and the JDK-8371889 fix was - * backported to 21.0.11 -- on all of those there is nothing to initialize and the {@code forName} - * calls simply fail and are ignored. + *

    The holder class was renamed across JDK versions, so it is selected by version (see {@link + * #jfrEventHolderClassName()}): {@code jdk.jfr.events.Handlers} on JDK 15-18, {@code + * jdk.jfr.events.EventConfigurations} on JDK 19-22. Earlier JDKs (including 11 LTS) predate the + * holder and JDK 23+ removed the eager-init pattern; on those this method does nothing. On + * patched JDKs (the JDK-8371889 fix was backported to 21.0.11) the JDK already initializes the + * holder safely during {@code FlightRecorder} startup, so forcing it here is a harmless no-op. * * @param loader class loader used to resolve the JFR classes (package-private for testing; see * JfrEventHolderInitForkedTest) */ static void initializeJfrEventHolderClass(final ClassLoader loader) { + final String holderClassName = jfrEventHolderClassName(); + if (holderClassName == null) { + return; // no eager-init holder on this JDK, so there is no deadlock to prevent + } try { // Register the JDK's built-in JFR events first, so the holder's below sees non-null // handlers instead of caching null. Class.forName("jdk.jfr.FlightRecorder", true, loader) .getMethod("getFlightRecorder") .invoke(null); - // Force the holder's . The class name depends on the JDK version. - try { - Class.forName("jdk.jfr.events.Handlers", true, loader); // JDK 15-18 - } catch (final ClassNotFoundException notJdk17Or18) { - Class.forName("jdk.jfr.events.EventConfigurations", true, loader); // JDK 19-22 - } + // Force the holder's . + Class.forName(holderClassName, true, loader); } catch (final Throwable ignored) { - // JFR unavailable/disabled, holder renamed or removed (JDK 23+), or initialization failed: in - // every case there is nothing (left) to do. We catch Throwable rather than Exception because - // forcing initialization can surface Errors such as ExceptionInInitializerError, - // NoClassDefFoundError or LinkageError. + // JFR unavailable/disabled or initialization failed: nothing (left) to do. We catch Throwable + // rather than Exception because forcing initialization can surface Errors such as + // ExceptionInInitializerError, NoClassDefFoundError or LinkageError. + } + } + + /** + * Returns the fully-qualified name of the JDK's JFR event-holder class for the running JVM, or + * {@code null} if this JDK has no such class. The class is selected by JDK version rather than by + * probing with {@link ClassNotFoundException} so the mapping is explicit: + * + *

      + *
    • JDK 15-18: {@code jdk.jfr.events.Handlers} + *
    • JDK 19-22: {@code jdk.jfr.events.EventConfigurations} + *
    • otherwise (JDK 14 and earlier predate the holder; JDK 23+ removed the eager-init + * pattern): {@code null} + *
    + */ + static String jfrEventHolderClassName() { + if (isJavaVersionAtLeast(15) && !isJavaVersionAtLeast(19)) { + return "jdk.jfr.events.Handlers"; + } + if (isJavaVersionAtLeast(19) && !isJavaVersionAtLeast(23)) { + return "jdk.jfr.events.EventConfigurations"; } + return null; } private static synchronized void registerSmapEntryEvent() { diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java index bd7cce7c551..1351a73a215 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java @@ -44,17 +44,13 @@ */ public class JfrEventHolderInitForkedTest { - private static final String[] HOLDER_CLASS_NAMES = { - "jdk.jfr.events.Handlers", // JDK 15-18 - "jdk.jfr.events.EventConfigurations" // JDK 19-22 - }; - @Test public void productionInitOrderingDoesNotPoisonHandlers() throws Exception { final ClassLoader loader = getClass().getClassLoader(); - // Find the holder class for this JDK without initializing it; skip if none exists (JDK 8, 23+). - final String holderName = findHolderClass(loader); + // The holder class (if any) is selected by JDK version; skip when this JDK has none (JDK 8, + // 23+). + final String holderName = Agent.jfrEventHolderClassName(); assumeTrue(holderName != null, "No JFR event-holder class on this JDK; nothing to test"); // Exercise the exact production path: FlightRecorder init first, then holder . @@ -81,16 +77,4 @@ public void productionInitOrderingDoesNotPoisonHandlers() throws Exception { "JFR handler fields were poisoned to null (holder initialized before FlightRecorder): " + nullFields); } - - private static String findHolderClass(final ClassLoader loader) { - for (final String name : HOLDER_CLASS_NAMES) { - try { - Class.forName(name, false, loader); // load only, do not initialize - return name; - } catch (final Throwable ignored) { - // not present on this JDK; try the next name - } - } - return null; - } } From 0d01fd1309d9c3e6825e4f8faff1c314901808d2 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Thu, 16 Jul 2026 17:31:57 +0200 Subject: [PATCH 9/9] Scope the JFR holder-class workaround to HotSpot The build pipeline's test_base:[semeru17] job failed: on JDK 17 the version check selected jdk.jfr.events.Handlers, but Eclipse OpenJ9 / IBM Semeru ships a different JFR implementation where the holder / Utils handler mechanism does not behave like HotSpot, so the regression test's non-null-handler assertion did not hold. (semeru8/semeru11 passed because those versions have no holder and the test skips.) JDK-8371889 is a HotSpot JFR bug, so gate jfrEventHolderClassName() on JavaVirtualMachine.isHotspot(): it now returns null on non-HotSpot VMs, making the production init a no-op there and the regression test skip. Verified the test still runs (not skipped) and passes on HotSpot JDK 17. Co-Authored-By: Claude Opus 4.8 --- .../src/main/java/datadog/trace/bootstrap/Agent.java | 8 ++++++++ .../trace/bootstrap/JfrEventHolderInitForkedTest.java | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index b3d0d36befc..555e027d498 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -1,5 +1,6 @@ package datadog.trace.bootstrap; +import static datadog.environment.JavaVirtualMachine.isHotspot; import static datadog.environment.JavaVirtualMachine.isJavaVersionAtLeast; import static datadog.environment.JavaVirtualMachine.isOracleJDK8; import static datadog.trace.api.Config.isExplicitlyDisabled; @@ -1024,8 +1025,15 @@ static void initializeJfrEventHolderClass(final ClassLoader loader) { *
  • otherwise (JDK 14 and earlier predate the holder; JDK 23+ removed the eager-init * pattern): {@code null} * + * + *

    Also returns {@code null} on non-HotSpot VMs: JDK-8371889 is a HotSpot JFR bug, and other + * VMs (e.g. Eclipse OpenJ9 / IBM Semeru) ship a different JFR implementation where this holder / + * {@code Utils} mechanism does not apply. */ static String jfrEventHolderClassName() { + if (!isHotspot()) { + return null; + } if (isJavaVersionAtLeast(15) && !isJavaVersionAtLeast(19)) { return "jdk.jfr.events.Handlers"; } diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java index 1351a73a215..ebe9e8443da 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/JfrEventHolderInitForkedTest.java @@ -39,8 +39,9 @@ * * *

    Requires {@code --add-opens jdk.jfr/jdk.jfr.events=ALL-UNNAMED} (configured in build.gradle) - * to read the holder's fields reflectively. Automatically skipped on JDKs without the holder class - * (JDK 8 and earlier, and JDK 23+ where the eager-init pattern was removed). + * to read the holder's fields reflectively. Automatically skipped where there is no holder to + * initialize: JDK 14 and earlier, JDK 23+ (eager-init pattern removed), and non-HotSpot VMs such as + * Eclipse OpenJ9 / IBM Semeru (different JFR implementation). */ public class JfrEventHolderInitForkedTest {