[Java.Interop] Defer exception creation in TryLoadClassWithFallback to fix global ref leak#1410
Merged
jonathanpeppers merged 2 commits intomainfrom Apr 21, 2026
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates JniEnvironment.Types class lookup logic to avoid creating managed JavaException instances (and thus JNI global refs) unless an exception must actually be thrown, addressing a global reference leak/churn scenario in the UTF-8 FindClass fallback path.
Changes:
- Defer managed exception materialization in
TryLoadClassWithFallbackuntilthrowOnError=trueand all fallback attempts fail. - Convert dot-separated class names to JNI slash form before calling raw
FindClass(string and UTF-8 overload paths). - Refactor UTF-8
TryFindClassto share a pointer-based helper and perform dot-to-slash conversion with stack/heap buffer as needed.
…o avoid unnecessary global refs Previously, TryLoadClassWithFallback eagerly called GetExceptionForThrowable before trying Class.forName(), creating a managed JavaException (with a JNI global ref, getMessage(), getCause(), getStackTrace()) even when Class.forName() would succeed immediately after. This was especially costly on the UTF-8 FindClass path introduced in PR #1407, which now falls back through Class.forName() where it previously did not. The fix defers managed exception creation: try Class.forName() first using only raw JNI operations, and only materialize the managed exception if we actually need to throw. In the common case (Class.forName succeeds or throwOnError=false), no managed exception is created and no global ref is allocated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7c16994 to
40dacc5
Compare
…refs Adds regression tests for both UTF-8 and string TryFindClass overloads, verifying that repeated lookups of non-existent classes do not leak JNI global references. These tests exercise the TryLoadClassWithFallback code path (throwOnError=false) that was the source of the leak. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
40dacc5 to
ebbdcee
Compare
jonathanpeppers
approved these changes
Apr 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the JNI global reference leak introduced by #1407 by deferring managed exception creation in
TryLoadClassWithFallback.Root Cause
PR #1407 added a
Class.forName()fallback to the UTF-8FindClasspath, sharingTryLoadClassWithFallbackbetween thestringandReadOnlySpan<byte>overloads.The problem:
TryLoadClassWithFallbackeagerly callsGetExceptionForThrowableon theFindClassfailure throwable before tryingClass.forName(). This creates a fullJavaExceptionmanaged object which:ConstructPeer)getMessage(),getCause()(recursively creating moreJavaExceptions with their own global refs), andgetStackTrace()Class.forName()succeeds immediately after, or whenthrowOnError=falseBefore #1407, the UTF-8 path never entered this code, so no managed exceptions were created for failed
FindClasscalls. After #1407, every UTF-8 class lookup failure eagerly creates (and disposes) managed exceptions with global refs — causing the global ref churn and OOM observed in Debug mode on Android.Fix
Defer managed exception creation: try
Class.forName()first using only raw JNI operations, and only materialize the managed exception if we actually need to throw.Class.forName()succeedsJavaException+ global ref, thenDispose()throwOnError=false(both fail)JavaException+ global ref, thenDispose()throwOnError=true(both fail)JavaException+ throwJavaException+ throwTesting
Java.Interop-Testspass, including all 16 UTF-8FindClasstests added in Fix UTF-8 JniType class lookup fallback #1407.TryFindClass_Utf8_DoesNotLeakGlobalRefs,TryFindClass_String_DoesNotLeakGlobalRefs) that assertGlobalReferenceCountis unchanged after aTryFindClasscall for a non-existent class.