You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We embed GraalPy with polyglot isolates: ONE shared Engine
(SandboxPolicy.UNTRUSTED, engine.MaxIsolateMemory=1GB) serving
short-lived one-shot Contexts. When each context evaluates a handful
of stdlib imports before its real work, every Context.close() leaves
~4.5-5 MB of REACHABLE objects in the isolate's guest heap. The live
set after Full GC grows monotonically (PrintGC: 23 -> 1031 MB across
~200 cycles) until the isolate dies with PolyglotException: MemoryError:
polyglot 25.1.3: death between cycle ~150-175;
polyglot 25.2.4: better (~2x more cycles) but the same cliff —
death between cycle ~300-325, with the average cycle degrading from
~72 ms to ~300 ms as the isolate spends most of its time in
back-to-back Full GCs.
The control experiment that separates this from "you are just creating
many contexts": the same cycle with context.eval("python", "x = 1")
instead of the imports is completely flat for 600 cycles — context
churn itself does not leak. Executing imports is the driver. Preempting
the usual questions: every cycle evaluates the identical source; no C
extensions, pure stdlib (datetime, decimal, ipaddress, re, itertools); a single import decimal leaks more slowly (survives
250 cycles); we cannot easily take a guest heap dump out of an isolate,
the PrintGC live-set growth is our referent evidence — happy to run
any diagnostics you suggest.
Raising MaxIsolateMemory to 4GB just multiplies the cycle count
before the same cliff. Reproduces identically under SandboxPolicy.TRUSTED + engine.SpawnIsolate=true (so not
UNTRUSTED-specific; that variant is where the PrintGC numbers come
from, TRUSTED allows engine.IsolateOption.PrintGC=true).
Not a duplicate of oracle/graal#8927 (host JVM heap static cache,
implicit per-context engines, no imports involved, closed completed
2026-02 — predates 25.2.4 where we still reproduce). Sibling but
distinct from oracle/graal#14252 we filed earlier: that one is about
isolates of already-CLOSED engines never being unmapped by the host;
this one is about retention inside a LIVE isolate while its engine
keeps serving. In our production the two compounded.
Operating system
macOS
CPU architecture
ARM64
GraalPy version
25.1.3 and 25.2.4 (community, python-isolate-*-community artifacts from Maven Central)
java ImportLeak 400 — dies with MemoryError at ~150-175 (25.1.3)
or ~300-325 (25.2.4); watch the per-cycle time jump ~4-14x shortly
before death (the isolate enters back-to-back Full GCs).
java ImportLeak 600 control — same lifecycle, x = 1 instead of
imports: 600 cycles, flat timings, no failure.
Expected behavior
Closing a context releases (or makes collectable) the state created by
executing stdlib imports inside it; a shared engine should be able to
serve an unbounded number of short-lived contexts within a bounded
isolate heap. The documented one-context-per-request embedding pattern
currently has a hard lifetime of ~150-300 requests per 1GB isolate,
with a multi-second GC agony phase before each death.
Stack trace
Exception in thread "main" MemoryError: MemoryError
at org.graalvm.polyglot.PolyglotException.<init>(PolyglotException.java:122)
at com.oracle.truffle.polyglot.PolyglotImpl.guestToHostException(PolyglotImpl.java:1245)
at com.oracle.truffle.polyglot.isolate.PolyglotMarshallerConfig$ThrowableMarshaller.createPolyglotException(PolyglotMarshallerConfig.java:2296)
at org.graalvm.nativebridge.ForeignException.throwOriginalException(ForeignException.java:135)
...
Additional context
Also reproduced on linux-aarch64 (Ubuntu 24.04, OpenJDK 21) with
25.2.4 — same growth curve and MemoryError. Our production service
(linux-amd64) lived through the matching death pattern (isolates
dying every ~150-200 contexts) until we minimized per-context
imports.
PrintGC evidence (TRUSTED + engine.SpawnIsolate=true + engine.IsolateOption.PrintGC=true, 25.1.3): live set after Full GC
grows 23 -> 131 -> 272 -> 400 -> 664 -> 795 -> 914 -> 1031 MB across
~200 cycles; a Full GC near the ceiling reclaims ~5%; 78% of wall
time is GC pauses in the final phase.
Describe the bug
We embed GraalPy with polyglot isolates: ONE shared
Engine(
SandboxPolicy.UNTRUSTED,engine.MaxIsolateMemory=1GB) servingshort-lived one-shot
Contexts. When each context evaluates a handfulof stdlib imports before its real work, every
Context.close()leaves~4.5-5 MB of REACHABLE objects in the isolate's guest heap. The live
set after Full GC grows monotonically (PrintGC: 23 -> 1031 MB across
~200 cycles) until the isolate dies with
PolyglotException: MemoryError:death between cycle ~300-325, with the average cycle degrading from
~72 ms to ~300 ms as the isolate spends most of its time in
back-to-back Full GCs.
The control experiment that separates this from "you are just creating
many contexts": the same cycle with
context.eval("python", "x = 1")instead of the imports is completely flat for 600 cycles — context
churn itself does not leak. Executing imports is the driver. Preempting
the usual questions: every cycle evaluates the identical source; no C
extensions, pure stdlib (
datetime,decimal,ipaddress,re,itertools); a singleimport decimalleaks more slowly (survives250 cycles); we cannot easily take a guest heap dump out of an isolate,
the PrintGC live-set growth is our referent evidence — happy to run
any diagnostics you suggest.
Raising
MaxIsolateMemoryto 4GB just multiplies the cycle countbefore the same cliff. Reproduces identically under
SandboxPolicy.TRUSTED+engine.SpawnIsolate=true(so notUNTRUSTED-specific; that variant is where the PrintGC numbers come
from, TRUSTED allows
engine.IsolateOption.PrintGC=true).Not a duplicate of oracle/graal#8927 (host JVM heap static cache,
implicit per-context engines, no imports involved, closed completed
2026-02 — predates 25.2.4 where we still reproduce). Sibling but
distinct from oracle/graal#14252 we filed earlier: that one is about
isolates of already-CLOSED engines never being unmapped by the host;
this one is about retention inside a LIVE isolate while its engine
keeps serving. In our production the two compounded.
Operating system
macOS
CPU architecture
ARM64
GraalPy version
25.1.3 and 25.2.4 (community,
python-isolate-*-communityartifacts from Maven Central)JDK version
OpenJDK 23.0.2 (macOS runs), OpenJDK 21.0.11 (Linux runs)
Context configuration
Steps to reproduce
Full single-file reproducer (Gradle deps:
org.graalvm.polyglot:polyglot:25.2.4,org.graalvm.python:python:25.2.4,org.graalvm.python:python-isolate-<platform>-community:25.2.4):java ImportLeak 400— dies withMemoryErrorat ~150-175 (25.1.3)or ~300-325 (25.2.4); watch the per-cycle time jump ~4-14x shortly
before death (the isolate enters back-to-back Full GCs).
java ImportLeak 600 control— same lifecycle,x = 1instead ofimports: 600 cycles, flat timings, no failure.
Expected behavior
Closing a context releases (or makes collectable) the state created by
executing stdlib imports inside it; a shared engine should be able to
serve an unbounded number of short-lived contexts within a bounded
isolate heap. The documented one-context-per-request embedding pattern
currently has a hard lifetime of ~150-300 requests per 1GB isolate,
with a multi-second GC agony phase before each death.
Stack trace
Additional context
25.2.4 — same growth curve and MemoryError. Our production service
(linux-amd64) lived through the matching death pattern (isolates
dying every ~150-200 contexts) until we minimized per-context
imports.
engine.SpawnIsolate=true+engine.IsolateOption.PrintGC=true, 25.1.3): live set after Full GCgrows 23 -> 131 -> 272 -> 400 -> 664 -> 795 -> 914 -> 1031 MB across
~200 cycles; a Full GC near the ceiling reclaims ~5%; 78% of wall
time is GC pauses in the final phase.
N contexts (a warm engine rebuild is only ~2-4 ms) — which is what
led us to the sibling issue Polyglot isolates: Engine.close() silently skips isolate teardown after any eval — ~32GB reservation and committed heap stay mapped graal#14252.