Skip to content

Redline native lifetime - #258

Merged
andreaTP merged 20 commits into
bytecodealliance:mainfrom
andreaTP:redline-native-lifetime
Oct 30, 2026
Merged

andreaTP merged 20 commits into
bytecodealliance:mainfrom
andreaTP:redline-native-lifetime

Conversation

@andreaTP

@andreaTP andreaTP commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #203. An alternative to #236: Redline's native state is freed by the garbage collector once nothing references it, like everything else in Endive, instead of being owned and freed by close().

Why

Compiled code reaches imported memories and tables by raw address, so freeing them in close() needs ownership tracking: ordered closes, refused closes, a call/close handshake. And an instance nobody closes still leaks, which is #203. The JVM already knows who uses what. The only thing stopping the collector was that upcall stubs and closures held their machine strongly.

How

  • Panama allocates in automatic arenas. On jffi (Java 11), each object registers a Cleaner action and fences its accesses.
  • Upcalls hold their machine weakly. A running call keeps it reachable.
  • A table keeps the instances whose functions it holds, and lets go of those no entry names any more.
  • close() stops the instance. It never refuses and frees nothing, so held memories, tables and globals keep working.
  • The collector can't see native memory, so Redline asks for a collection as it grows (-Dendive.redline.gcThreshold), as the JDK does for direct buffers.

Fixed along the way:

  • jffi leaked a CallContext per function per instance.
  • Unbounded tables no longer take 24 MB each.
  • A failed table.grow fill left entries unset.
  • Two instances could adopt the same host global.
  • jffi ignored a failed mprotect.

Behavior changes

  • close() frees nothing; native memory goes once unreferenced. With -XX:+DisableExplicitGC that waits for a collection that happens anyway.
  • The native code format is now version 5, so code serialized by an earlier version must be compiled again.
  • Instances nested through host imports on one thread share one stack budget.
  • withMemoryLimits rejects limits that change whether the memory is shared.

Measured

Peak RSS when building 200 Javy instances, about 11.5 MB of native memory each, on JDK 25:

#236, closed dropped closed closed, no pacing
jffi 592 MB 523 MB 527 MB 2736 MB
Panama 789 MB 811 MB 787 MB 2087 MB

Without close(), #236 still holds 2.6 GB after System.gc(); here everything is freed. With long-lived instances under G1, Serial and ZGC, native memory stays within about twice the live set, at 0.5 to 2 ms of extra collection time per instance.

Testing

Both runners run the Redline tests and the spec suite on JDK 25, and the jffi runner also runs on JDK 21. New lifecycle tests check:

  • dropped and closed instances are freed;
  • upcalls and host globals don't pin their instance;
  • close() works from inside a call and from another thread;
  • held memories and tables keep working after their instances are collected.

@andreaTP
andreaTP force-pushed the redline-native-lifetime branch from 786d1b9 to e0cba00 Compare October 27, 2026 15:26
@andreaTP

andreaTP commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

I think this is the way to go, I'll close #236 and I'll move on with this design, redline is still experimental, so we can iterate if we find any gap.

@andreaTP
andreaTP marked this pull request as ready for review October 28, 2026 12:57

@andreaTP andreaTP left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, ready to merge when CI is green.

@andreaTP
andreaTP merged commit 847e866 into bytecodealliance:main Oct 30, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redline: native resources of an unclosed Instance are never released (no Cleaner fallback)

1 participant