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