Feedback on JEP 454: The "Refactoring Hazard" of the Arena API
Maurizio Cimadamore
maurizio.cimadamore at oracle.com
Mon Jan 19 15:50:45 UTC 2026
Hi,
thanks for the feedback. The decision of whether to lump or split
various kind of arenas has defintively been on our mind while designing
the API.
First, let me point out that I find your connection to integrity by
default a bit excessive. You can already have memory leaks in Java in
all sort of ways -- without the need to resort to off-heap memory. A
program with a memory leak doesn't run into undefined behavior, nor does
it undermine memory safety. It is just a badly written (and possibly
very inefficient) program. (Note that Rust has a similar stance [1] --
preventing memory leaks is *not* among the guarantees provided by the
Rust type system).
More to your point, our experience with trying to provide an API that
more clearly separated between managed and unmanaged lifetime always led
to the feeling that, while more formally correct, more splitty APIs
weren't really improving the situation much -- and, at the same time,
would make discoverability of the API worse, by scattering arena
factories across the FFM API.
Let's say, for argument sake, that we had two Arena types -- Arena and
ManagedArena. ManagedArena would have a close method, while Arena would
not. Let's say that, for ease of use, one would start from a plain Arena:
```
Arena arena = Arena.ofAuto();
arena.allocate(...)
foo(arena);
```
But later on, as you point out, one realizes that more control over
deallocation is required, and a confined arena is needed. Let's say we
change only the first factory call:
```
Arena arena = ManagedArena.ofConfined();
arena.allocate(...)
foo(arena);
```
The rest of the code would still compile perfectly. So, having a more
splitty hierarchy here doesn't really buy a lot of extra safety, as you
can still refactor your code in ways that leaves deallocation behind.
That is, unless you go completely overboard and make Arena and
ManagedArena incompatible, which would be very bad, as now developers
would have to keep two unrelated abstractions in their minds at all
times (meaning the split would also affect client code) -- we tried this
in Java 20 and it didn't really work out. E.g. an important use case
such as Arena extensibility was badly hurt by the split move, as users
could define custom managed arenas, but they could not define custom
automatic arenas with more efficient allocation policies (which resulted
in tension/friction between Arena and SegmentAllocator).
Instead of lump vs. split, I think the crucial observation is this:
> Because these implementations throw an exception on |close()|,
> developers are actively incentivized (and practically forced) to avoid
> the |try-with-resources| pattern.
This has also been the subject of many discussions while designing the
API. There's basically two questions here:
* should close() be idempotent? E.g. should closing an already closed
arena succeed?
* should close() tolerate cases where the method is called on a managed
arena (with the subtext that, in such cases, close() is, again, a no-op)
When designing the API, we always erred on the side of catching user
bugs -- out of a desire to make a low-level API such as FFM as
deterministic and predictable as possible (e.g. close really means
close). In this light, closing an already closed arena seems like a bug
(double free?) so, throwing an exception seemed reasonable. And,
similarly, closing an arena externally managed also seemed suspicious,
as no real "free" occurs there. Although I concede that argument for the
former is less clear-cut than the latter -- e.g. even if close were to
be idempotent, the invariant that all segments associated with a closed
arena are no longer accessible would still be valid. On the other hand,
allowing to close a managed arena might create an expectation that its
segments are no longer accessible, whereas that's not the case.
There's no "free lunches" here -- either we make the API more easily
usable with try-with-resources _or_ we detect suspicious activity in
user code (such as a double close), but we can't do both.
Perhaps, to mitigate some of the concerns you have, some other avenues
could be explored:
* an IDE analysis could e.g. detect cases where a confined/shared arena
is used w/o a corresponding try-with-resource block
* some JFR event could be triggered when an confined/shared Arena
becomes unrechable and its scope is still alive (Netty's ByteBuf API has
something similar to this [2])
Summing up, after staring at this problem for a very long time, I don't
think there's a "perfect" Arena API that is rid of all the
aforementioned issues -- it's mostly a "pick your poison" situation.
Cheers
Maurizio
[1] - https://doc.rust-lang.org/book/ch15-06-reference-cycles.html
[2] -
https://netty.io/wiki/reference-counted-objects.html#troubleshooting-buffer-leaks
On 16/01/2026 23:49, Abraham Tehrani wrote:
>
> Dear Panama Dev Team,
>
> I would like to offer feedback on the current design of the |Arena|
> interface, specifically the decision to have |global()| and |ofAuto()|
> throw |UnsupportedOperationException| on |close()|.
>
> While I understand the desire for a unified interface, I believe this
> design creates a significant "Refactoring Hazard" that undermines the
> goal of *Integrity by Default*.
>
> *The Path to a Leak:* Most developers will naturally start with
> |global()| or |ofAuto()| for ease of use. Because these
> implementations throw an exception on |close()|, developers are
> actively incentivized (and practically forced) to avoid the
> |try-with-resources| pattern.
>
> Later, when the developer needs to optimize and switches to
> |ofConfined()| or |ofShared()|, they will perform a "drop-in"
> replacement of the implementation. Because |Arena| is a unified
> interface, the code will compile perfectly. However, the developer,
> now conditioned to treat Arenas as managed, will likely fail to wrap
> the new implementation in a |try-with-resources| block.
>
> *The Result:* A catastrophic off-heap memory leak.
>
> In almost any other Java API, refactoring toward more manual control
> is guarded by the compiler or the type system. Here, the "ease of use"
> of a single interface effectively masks a fundamental change in
> lifecycle responsibility.
>
> When dealing with /native memory/, *correctness and explicit lifecycle
> management must take precedence over interface uniformity.* We are
> trading a slightly "split" API for a very real "split" in runtime safety.
>
> The easiest way to use an API should also be the safest way.
>
> Respectfully and Humbly,
> Abraham Tehrani
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/panama-dev/attachments/20260119/bebcd9f1/attachment-0001.htm>
More information about the panama-dev
mailing list