Repository navigation
Add JavaVirtualMachine.current, a lock-free read that never creates a JVM - #26
Open
piercifani wants to merge 1 commit into
Open
piercifani wants to merge 1 commit into
piercifani wants to merge 1 commit into
Conversation
… JVM shared() takes a lock on every call and finds or creates a JVM when none is set. Libraries that look up the JVM on every JNI call pay for that lock, and 'is a JVM available?' checks end up launching one. current reads the published shared instance through an acquire load and returns nil when nothing has been set or adopted. shared() uses it as a fast path, and the thread-exit detach uses it so a thread exiting never creates a JVM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
JavaVirtualMachine.shared()is the only way to get the shared instance, and it has two properties that make it a poor fit for hot paths:JavaObjectHolder.forget()and_JNIMethodIDCache; Skip's bridging does it on every bridged call. On an Android emulator, routing a production Skip Fuse app's bridging benchmark throughshared()made it 1.09–1.33× slower on every case that crosses JNI (strings, structs, arrays). That was a debug build, one run per configuration.Change
public static var current: JavaVirtualMachine?: the published shared instance, read with an acquire load. Returnsnilif nothing was set or adopted, and never creates a JVM.setShared,setSharedJVM,forgetShared,shared(replace:), and adoption insideshared()) still goes through the existing lock and publishes the instance with a release store. Pointer-sized atomics are twostatic inlinehelpers inCSwiftJavaJNI, so this works on every platform without requiringSynchronization(macOS 15+).shared()returnscurrentwithout locking when an instance exists. Its behavior doesn't change: an existing instance was already returned regardless of the options passed.current, so a thread exiting never creates a JVM.Replacing or forgetting the shared instance releases the old one. As with
destroyJVM(), callers must not do that while other threads are still using it. The doc comment says so.Numbers
currentIsCheaperThanLockedRead)current3.3 nsshared()-per-call version: 1.09–1.33× slower than Skip's previous JNI layer (its own wrapper, not built on jni-core)Example cases from that benchmark:
shared()per call)current)[String]× 200 getter"After" was measured together with a follow-up change (application class loader applied on thread attach). That change runs once per attached thread and doesn't touch the per-call path.
Tests
JavaVirtualMachineCurrentTests:currentis identical toshared().Results:
swift:6.4.2-noble, OpenJDK 17)SWIFT_JAVA_JNI_TEST_JVM=1.🤖 Generated with Claude Code