From 62133fc24f5e3d9c876d01b339bb7b393be97e86 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:31:56 +0200 Subject: [PATCH 1/6] perf(core): Probe class availability without initializing the class LoadClass.loadClass used Class.forName(name) which initializes the class. Used purely for availability probing during init, this eagerly runs unrelated static initializers (e.g. Compose's Owner, the fragment integration). Use Class.forName(name, false, classLoader) so the class is only initialized lazily on first real use. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/java/io/sentry/util/LoadClass.java | 4 +- .../test/java/io/sentry/util/LoadClassTest.kt | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 sentry/src/test/java/io/sentry/util/LoadClassTest.kt diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index 1946ce8381f..c639f62b9c4 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -20,7 +20,9 @@ public class LoadClass { */ public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { try { - return Class.forName(clazz); + // Don't initialize the class just to probe for availability; it gets initialized lazily on + // first use. This avoids running unrelated static initializers during SDK init. + return Class.forName(clazz, false, LoadClass.class.getClassLoader()); } catch (ClassNotFoundException e) { if (logger != null) { logger.log(SentryLevel.INFO, "Class not available: " + clazz); diff --git a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt new file mode 100644 index 00000000000..0c8dacd1c54 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt @@ -0,0 +1,49 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class LoadClassTest { + @Test + fun `loadClass returns the class when it is available`() { + assertNotNull(LoadClass().loadClass("io.sentry.SentryEvent", null)) + } + + @Test + fun `loadClass returns null when the class is not available`() { + assertNull(LoadClass().loadClass("io.sentry.ThisClassDoesNotExist", null)) + } + + @Test + fun `isClassAvailable reflects whether the class is on the classpath`() { + val loadClass = LoadClass() + assertNotNull(loadClass.loadClass("io.sentry.SentryEvent", null)) + assertFalse( + loadClass.isClassAvailable("io.sentry.ThisClassDoesNotExist", null as io.sentry.ILogger?) + ) + } + + @Test + fun `loadClass does not run the static initializer of the probed class`() { + // Reading the flag initializes the flag holder, not the probe. + assertFalse(LoadClassNoInitFlag.initialized) + + // Obtaining the name via ::class.java does not initialize the probe either. + LoadClass().loadClass(LoadClassNoInitProbe::class.java.name, null) + + // Availability probing must not trigger the probe's static initializer. + assertFalse(LoadClassNoInitFlag.initialized) + } +} + +private object LoadClassNoInitFlag { + @JvmField var initialized = false +} + +private object LoadClassNoInitProbe { + init { + LoadClassNoInitFlag.initialized = true + } +} From 7a33c9c04ea2f84c683ee7523e5275f61d76dd54 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:32:38 +0200 Subject: [PATCH 2/6] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a1115f8ae..92cd4b4eec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Internal + +- Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635)) + ## 8.45.0 ### Features From 552d466d7843d7d23ec4d6f10aedf6cf9cb83f72 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:45:57 +0200 Subject: [PATCH 3/6] changelog: move init reflection entries to Performance --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92cd4b4eec6..ca6a5b91ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -### Internal +### Performance - Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635)) From f6074f1f3de875ab2aa40cd94fca93a3f55df0b9 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 29 Jun 2026 09:24:10 +0200 Subject: [PATCH 4/6] perf(core): Limit no-init class probing to isClassAvailable The previous change made loadClass itself skip class initialization, which affected callers that load a class to actually use it (NDK integration, OTEL span factory and scopes storage). Restore loadClass to its initializing behavior and confine the non-initializing probe to isClassAvailable, which is only ever used for classpath availability checks. This keeps SDK init cheap while leaving real-use callers unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/java/io/sentry/util/LoadClass.java | 36 ++++++++++++++++--- .../test/java/io/sentry/util/LoadClassTest.kt | 35 ++++++++++++++---- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index c639f62b9c4..2c39cace39b 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -12,17 +12,23 @@ public class LoadClass { /** - * Try to load a class via reflection + * Loads and initializes a class via reflection. Use this when you intend to actually use the + * class (e.g. instantiate it or invoke its methods). The returned class is fully initialized, so + * its static initializers run. To merely check whether a class is on the classpath, use {@link + * #isClassAvailable} instead, which avoids running those initializers. * * @param clazz the full class name * @param logger an instance of ILogger * @return a Class<?> if it's available, or null */ public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { + return loadClass(clazz, logger, true); + } + + private @Nullable Class loadClass( + final @NotNull String clazz, final @Nullable ILogger logger, final boolean initialize) { try { - // Don't initialize the class just to probe for availability; it gets initialized lazily on - // first use. This avoids running unrelated static initializers during SDK init. - return Class.forName(clazz, false, LoadClass.class.getClassLoader()); + return Class.forName(clazz, initialize, LoadClass.class.getClassLoader()); } catch (ClassNotFoundException e) { if (logger != null) { logger.log(SentryLevel.INFO, "Class not available: " + clazz); @@ -39,8 +45,19 @@ public class LoadClass { return null; } + /** + * Probes whether a class is on the classpath without initializing it. Use this for availability + * checks (e.g. deciding whether to register an integration); the class is not initialized, so its + * static initializers do not run until something actually uses it. This keeps SDK init cheap by + * not triggering unrelated initializers. If you need to use the class, use {@link #loadClass} + * instead. + * + * @param clazz the full class name + * @param logger an instance of ILogger + * @return true if the class is on the classpath + */ public boolean isClassAvailable(final @NotNull String clazz, final @Nullable ILogger logger) { - return loadClass(clazz, logger) != null; + return loadClass(clazz, logger, false) != null; } public boolean isClassAvailable( @@ -48,6 +65,15 @@ public boolean isClassAvailable( return isClassAvailable(clazz, options != null ? options.getLogger() : null); } + /** + * Like {@link #isClassAvailable}, but defers the (non-initializing) availability check until the + * result is first read. Use this when the check itself should not run during SDK init but only + * later, on first access. + * + * @param clazz the full class name + * @param logger an instance of ILogger + * @return a lazily-evaluated availability check + */ public LazyEvaluator isClassAvailableLazy( final @NotNull String clazz, final @Nullable ILogger logger) { return new LazyEvaluator<>(() -> isClassAvailable(clazz, logger)); diff --git a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt index 0c8dacd1c54..7a8bc802049 100644 --- a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt +++ b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt @@ -4,6 +4,7 @@ import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class LoadClassTest { @Test @@ -26,24 +27,44 @@ class LoadClassTest { } @Test - fun `loadClass does not run the static initializer of the probed class`() { + fun `isClassAvailable does not run the static initializer of the probed class`() { // Reading the flag initializes the flag holder, not the probe. - assertFalse(LoadClassNoInitFlag.initialized) + assertFalse(IsClassAvailableNoInitFlag.initialized) // Obtaining the name via ::class.java does not initialize the probe either. - LoadClass().loadClass(LoadClassNoInitProbe::class.java.name, null) + LoadClass() + .isClassAvailable(IsClassAvailableNoInitProbe::class.java.name, null as io.sentry.ILogger?) // Availability probing must not trigger the probe's static initializer. - assertFalse(LoadClassNoInitFlag.initialized) + assertFalse(IsClassAvailableNoInitFlag.initialized) + } + + @Test + fun `loadClass runs the static initializer of the loaded class`() { + assertFalse(LoadClassInitFlag.initialized) + + LoadClass().loadClass(LoadClassInitProbe::class.java.name, null) + + assertTrue(LoadClassInitFlag.initialized) + } +} + +private object IsClassAvailableNoInitFlag { + @JvmField var initialized = false +} + +private object IsClassAvailableNoInitProbe { + init { + IsClassAvailableNoInitFlag.initialized = true } } -private object LoadClassNoInitFlag { +private object LoadClassInitFlag { @JvmField var initialized = false } -private object LoadClassNoInitProbe { +private object LoadClassInitProbe { init { - LoadClassNoInitFlag.initialized = true + LoadClassInitFlag.initialized = true } } From 5b872a701bc875e8b20b9a251d779ae614b282c0 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:33:02 +0200 Subject: [PATCH 5/6] perf(core): Cache class lookups and collapse double probes Class availability is fixed for the lifetime of the process, so cache LoadClass results to avoid repeated Class.forName lookups (and the exceptions thrown for absent classes) when the same class is probed more than once. Also collapse the isClassAvailable-then-loadClass double probe in the OpenTelemetry span/scopes factories into a single loadClass call. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/io/sentry/ScopesStorageFactory.java | 33 +++++++++---------- .../java/io/sentry/SpanFactoryFactory.java | 33 +++++++++---------- .../main/java/io/sentry/util/LoadClass.java | 22 ++++++++++++- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/sentry/src/main/java/io/sentry/ScopesStorageFactory.java b/sentry/src/main/java/io/sentry/ScopesStorageFactory.java index 37c0acf2314..f09f0130c55 100644 --- a/sentry/src/main/java/io/sentry/ScopesStorageFactory.java +++ b/sentry/src/main/java/io/sentry/ScopesStorageFactory.java @@ -23,24 +23,23 @@ public final class ScopesStorageFactory { private static @NotNull IScopesStorage createInternal( final @NotNull LoadClass loadClass, final @NotNull ILogger logger) { if (Platform.isJvm()) { - if (loadClass.isClassAvailable(OTEL_SCOPES_STORAGE, logger)) { - Class otelScopesStorageClazz = loadClass.loadClass(OTEL_SCOPES_STORAGE, logger); - if (otelScopesStorageClazz != null) { - try { - final @Nullable Object otelScopesStorage = - otelScopesStorageClazz.getDeclaredConstructor().newInstance(); - if (otelScopesStorage instanceof IScopesStorage) { - return (IScopesStorage) otelScopesStorage; - } - } catch (InstantiationException e) { - // TODO log - } catch (IllegalAccessException e) { - // TODO log - } catch (InvocationTargetException e) { - // TODO log - } catch (NoSuchMethodException e) { - // TODO log + final @Nullable Class otelScopesStorageClazz = + loadClass.loadClass(OTEL_SCOPES_STORAGE, logger); + if (otelScopesStorageClazz != null) { + try { + final @Nullable Object otelScopesStorage = + otelScopesStorageClazz.getDeclaredConstructor().newInstance(); + if (otelScopesStorage instanceof IScopesStorage) { + return (IScopesStorage) otelScopesStorage; } + } catch (InstantiationException e) { + // TODO log + } catch (IllegalAccessException e) { + // TODO log + } catch (InvocationTargetException e) { + // TODO log + } catch (NoSuchMethodException e) { + // TODO log } } } diff --git a/sentry/src/main/java/io/sentry/SpanFactoryFactory.java b/sentry/src/main/java/io/sentry/SpanFactoryFactory.java index f0e3fcbb3c7..aae97851bc3 100644 --- a/sentry/src/main/java/io/sentry/SpanFactoryFactory.java +++ b/sentry/src/main/java/io/sentry/SpanFactoryFactory.java @@ -15,24 +15,23 @@ public final class SpanFactoryFactory { public static @NotNull ISpanFactory create( final @NotNull LoadClass loadClass, final @NotNull ILogger logger) { if (Platform.isJvm()) { - if (loadClass.isClassAvailable(OTEL_SPAN_FACTORY, logger)) { - Class otelSpanFactoryClazz = loadClass.loadClass(OTEL_SPAN_FACTORY, logger); - if (otelSpanFactoryClazz != null) { - try { - final @Nullable Object otelSpanFactory = - otelSpanFactoryClazz.getDeclaredConstructor().newInstance(); - if (otelSpanFactory instanceof ISpanFactory) { - return (ISpanFactory) otelSpanFactory; - } - } catch (InstantiationException e) { - // TODO log - } catch (IllegalAccessException e) { - // TODO log - } catch (InvocationTargetException e) { - // TODO log - } catch (NoSuchMethodException e) { - // TODO log + final @Nullable Class otelSpanFactoryClazz = + loadClass.loadClass(OTEL_SPAN_FACTORY, logger); + if (otelSpanFactoryClazz != null) { + try { + final @Nullable Object otelSpanFactory = + otelSpanFactoryClazz.getDeclaredConstructor().newInstance(); + if (otelSpanFactory instanceof ISpanFactory) { + return (ISpanFactory) otelSpanFactory; } + } catch (InstantiationException e) { + // TODO log + } catch (IllegalAccessException e) { + // TODO log + } catch (InvocationTargetException e) { + // TODO log + } catch (NoSuchMethodException e) { + // TODO log } } } diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index 2c39cace39b..52939e4535f 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -4,6 +4,8 @@ import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,6 +13,16 @@ @Open public class LoadClass { + /** Sentinel cached for class names that are known to be unavailable. */ + private static final Object NOT_AVAILABLE = new Object(); + + /** + * Whether a class is on the classpath does not change during the lifetime of the process, so + * results are cached to avoid repeated {@link Class#forName} lookups (and the exceptions they + * throw for absent classes) when the same class is probed more than once. + */ + private static final Map CLASSES = new ConcurrentHashMap<>(); + /** * Loads and initializes a class via reflection. Use this when you intend to actually use the * class (e.g. instantiate it or invoke its methods). The returned class is fully initialized, so @@ -27,9 +39,17 @@ public class LoadClass { private @Nullable Class loadClass( final @NotNull String clazz, final @Nullable ILogger logger, final boolean initialize) { + final @Nullable Object cached = CLASSES.get(clazz); + if (cached != null) { + return cached == NOT_AVAILABLE ? null : (Class) cached; + } try { - return Class.forName(clazz, initialize, LoadClass.class.getClassLoader()); + final Class loadedClass = + Class.forName(clazz, initialize, LoadClass.class.getClassLoader()); + CLASSES.put(clazz, loadedClass); + return loadedClass; } catch (ClassNotFoundException e) { + CLASSES.put(clazz, NOT_AVAILABLE); if (logger != null) { logger.log(SentryLevel.INFO, "Class not available: " + clazz); } From e6d30e88c0da2e2f85325f86d6e183186764eedf Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 29 Jun 2026 09:27:58 +0200 Subject: [PATCH 6/6] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6a5b91ec7..70aed91cb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Performance - Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635)) +- Cache reflective class lookups and avoid double-probing during SDK init ([#5636](https://github.com/getsentry/sentry-java/pull/5636)) ## 8.45.0