From 3471b23d87dabac685c84e9debb1840f2017fe41 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:33:02 +0200 Subject: [PATCH 1/3] 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 | 21 +++++++++++- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/sentry/src/main/java/io/sentry/ScopesStorageFactory.java b/sentry/src/main/java/io/sentry/ScopesStorageFactory.java index 37c0acf231..f09f0130c5 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 f0e3fcbb3c..aae97851bc 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 c639f62b9c..0bc203356e 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<>(); + /** * Try to load a class via reflection * @@ -19,11 +31,18 @@ public class LoadClass { * @return a Class<?> if it's available, or null */ public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { + final @Nullable Object cached = CLASSES.get(clazz); + if (cached != null) { + return cached == NOT_AVAILABLE ? null : (Class) cached; + } 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()); + final Class loadedClass = Class.forName(clazz, false, 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 513d73f44dc4e170de9aa98638730bab687bc6da Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:33:38 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92cd4b4eec..adc39a8519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Internal - 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 From 6672a4e7007f98d79e204d94f81dbb4dbb3997a6 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 25 Jun 2026 13:45:55 +0200 Subject: [PATCH 3/3] 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 adc39a8519..70aed91cb5 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)) - Cache reflective class lookups and avoid double-probing during SDK init ([#5636](https://github.com/getsentry/sentry-java/pull/5636))