Reservar 5 bytes de memoria nativa en un Arena.ofConfined() pasa de 15,9 a 1,05 nanosegundos en un Apple M4. Es la cifra más llamativa de los pooled confined arenas, una optimización que llegará con JDK 28 y que, según el equipo de Java de Oracle, recorta la latencia de las reservas pequeñas fuera del heap hasta un 90 % en Linux, macOS y Windows. No hay API nueva ni que cambiar una línea: el código que ya usa la API Foreign Function & Memory (FFM) irá más rápido al actualizar.
Por qué una reserva de 5 bytes era cara
El patrón más habitual con FFM es abrir un arena confinado alrededor de una llamada nativa, reservar un hueco para el resultado (un int, un puntero, una cadena corta) y cerrarlo al salir del try. Hasta ahora, incluso esa reserva mínima pasaba por el asignador nativo y registraba una acción de limpieza, y ese papeleo podía costar más que usar la memoria.
Lo que justifica el cambio es la distribución real: en una ejecución instrumentada de un conjunto de tests, más del 99,99 % de los arenas confinados que reservaban memoria usaban menos de 64 bytes. Con esa forma, un pool pequeño y perezoso resuelve casi todos los casos.
Cómo funciona el pool
Cada hilo de plataforma mantiene, solo si hace falta, una caché de pools de memoria nativa: por defecto cuatro de 64 bytes. El arena no coge un pool al crearse, sino con la primera reserva que cabe en él, así que los arenas que nunca reservan no cuestan nada. A partir de ahí, las reservas se van cortando una detrás de otra dentro del pool, que pertenece en exclusiva a ese arena; los arenas anidados quedan aislados entre sí.
Lo que no cabe, o pide una alineación que el pool no garantiza, sigue el camino de siempre. Al cerrar el arena, la parte usada se pone a cero y el pool vuelve a la caché del hilo, o se libera si la caché está llena. Según el pull request que lo integró en la rama principal de OpenJDK el 3 de septiembre de 2026, el tamaño se puede ajustar con una propiedad del sistema interna y sin soporte (hasta ocho pools por hilo, de 8 bytes a 1 MiB, y un valor negativo lo desactiva). Al ser interna, puede cambiar o desaparecer sin aviso.
Con hilos virtuales el diseño evita crear una caché en cada uno, lo que acabaría con su huella mínima. El hilo virtual toma prestado un pool de su hilo portador, queda fijado (pinned) solo durante ese traspaso y luego puede migrar con el arena abierto; al cerrarlo, el pool vuelve a la caché del portador en el que esté montado entonces.
Las cifras, antes y después
Medidas con JMH sobre reservas de 5 y 20 bytes:
| Plataforma | 5 bytes | 20 bytes |
|---|---|---|
| Linux AArch64 | 12,3x | 8,6x |
| Linux x64 | 6,8x | 7,8x |
| macOS AArch64 | 18,6x | 16,8x |
| Windows x64 | 16,8x | 18,9x |
En el M4, con hilos virtuales, la reserva de 5 bytes baja de 15,7 a 3,2 ns: menos ganancia por el manejo del portador, pero sigue siendo casi cinco veces más rápida. Por encima de 64 bytes no hay cambio: a 100 bytes las dos variantes rondan los 19 ns porque usan el camino normal.
Lo que importa para producción es dónde pesa esto. Se nota en los bindings FFM que abren un arena por llamada, en los envoltorios generados al estilo de jextract y en código sensible a la latencia que hace muchas llamadas nativas baratas. Si tu servicio hace una llamada nativa por petición, la mejora existe pero no se verá en el p99. Solo aplica a arenas confinados: los demás tipos de arena no cambian.
Hasta ahora, quien necesitaba quitarse este coste tenía que reutilizar a mano un arena o un segmento de larga vida y repartirlo, perdiendo parte de la garantía de que todo desaparece al salir del try. Con JDK 28 el patrón sencillo y seguro pasa a ser también el rápido.
El cambio de comportamiento que sí hay
La vida de los segmentos no cambia: al cerrar el arena dejan de ser accesibles y la memoria nueva llega a cero, como exige la API. Lo que cambia es la vida de la reserva nativa de debajo: cerrar un arena puede devolver la memoria a la caché del JDK en vez de llamar a free. Si tienes herramientas o tests que cuentan llamadas a malloc y free para detectar fugas en código FFM, dejarán de cuadrar uno a uno con las reservas del arena.
Cómo medirlo en tu código
JDK 28 tiene prevista su versión general el 23 de marzo de 2027; hasta entonces se puede probar con las builds de acceso anticipado de jdk.java.net/28. Un benchmark mínimo para comparar con tu JDK actual:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ConfinedAlloc {
@Param({"8", "32", "128"})
long size;
@Benchmark
public long allocateAndClose() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(size);
return segment.byteSize();
}
}
}Ejecútalo con tu JDK de producción y con la build de JDK 28 sobre la misma máquina. Antes de contar con la mejora:
- Mide tu ruta real, no solo el microbenchmark: la ganancia depende de qué parte de la llamada nativa se iba en reservar y liberar.
- Revisa si algún test o profiler de memoria nativa asume que cada
allocateacaba en unmalloc. - Comprueba cuántas reservas de tus bindings superan los 64 bytes: esas seguirán igual.
Fuentes
- Allocating Off-Heap Memory: Typically 6-18x Faster in JDK 28 — Inside Java
- 8385697: Add a Pooled Confined Arena — OpenJDK (GitHub)
- JDK Releases — java.com
- JDK 28 Early-Access Builds — OpenJDK