fix: stop leaking a JNI global ref per ArrayBuffer passed to Java - #2062
dpwhittaker wants to merge 1 commit into
Conversation
The first time a JS ArrayBuffer, SharedArrayBuffer or typed array crosses to Java, JsArgConverter and JsArgToArrayConverter wrap it in a direct ByteBuffer and take a JNI global reference to it that is never deleted. GetOrCreateObjectId already keeps the buffer strongly reachable from Java for as long as the JS object lives, so the extra reference only pins it: every distinct buffer stays in the global reference table for the life of the process. Because JSObjectFinalizer keeps a JS object alive while its Java counterpart is, the ArrayBuffer's memory is never freed either. An app that hands Java a fresh buffer per frame overflows ART's 51200-entry table within hours and aborts with "JNI ERROR (app bug): global reference table overflow". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughBoth JavaScript-to-Java buffer conversion paths now register buffers with ObjectManager without first creating JNI global references. New stress tests exercise repeated buffer conversions and check the resulting Java-held values. ChangesBuffer JNI reference handling
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Suggested reviewers: Merge Risk: ⚪ Minimal · up to This change stops buffers passed to Java from piling up as JNI global references, so the global-reference table should no longer overflow. No merge-blocking risk was found. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 1 system. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit hops past buffers bright Comment |
Description
When a JS
ArrayBuffer,SharedArrayBufferor typed array crosses to Java for the first time,JsArgConverter::ConvertArgwraps it in a directByteBuffer, then callsenv.NewGlobalRef(buffer)and never deletes the result.JsArgToArrayConverter::ConvertArghas the same code, and is used for Java array elements, overload resolution and values returned to Java.GetOrCreateObjectIdalready registers the buffer in the runtime's strong instances for as long as the JS object lives, so the extra reference does nothing except pin the buffer.Two consequences:
JNI ERROR (app bug): global reference table overflow (max=51200), and the dump shows ~50,000java.nio.DirectByteBufferentries.JSObjectFinalizerkeeps a JS object alive while its Java counterpart is alive, and the pinnedDirectByteBuffernever dies.Passing the same ArrayBuffer again doesn't leak, because the existing link is reused. So the leak is one reference per distinct buffer. That matters for any app that hands Java a fresh buffer per event. We hit it in an app that passes each display frame (pixels plus glyph data) to Java as new ArrayBuffers. It aborted after 2.75 to 10.6 hours of use, at 1.4 GB RSS.
The fix removes the
NewGlobalRefcall in both converters.Does your pull request have unit tests?
Yes. Two tests in
byte-buffer-test.jseach pass 60,000 fresh buffers to Java, more than the table can hold: one as method arguments (alternatingArrayBufferandUint8Array), one as Java array elements.Run on a Galaxy Z Fold7 (arm64, Android 17) with the runtime built from this branch:
DirectByteBufferglobal refs)Also checked in the affected app, by heap-dumping it and counting JNI global roots:
DirectByteBuffers. Unpatched, there were +1,000, and they survived a forcedgc().🤖 Generated with Claude Code
Summary by CodeRabbit