L'API Foreign Function & Memory (FFM) introduce MemorySegment per accedere in modo sicuro alla memoria off-heap. Ogni segmento è associato a una MemorySession (o Arena nelle versioni più recenti) che definisce il suo ciclo di vita. Quando un'arena viene chiusa, il layer ScopedMemoryAccess contrassegna tutti i segmenti associati come "non vivi."
Qualsiasi tentativo di accesso successivo attiva un controllo di ScopedMemoryAccess.Scope che genera immediatamente un IllegalStateException. Per impedire che il garbage collector recuperi un segmento mentre un'operazione nativa è in corso, la JVM impiega implicitamente i significati di reachabilityFence. Il compilatore inserisce barriere di mantenimento all'interno di confini critici, garantendo che l'oggetto segmento rimanga fortemente raggiungibile fino al completamento della chiamata nativa.
Questa coordinazione consente una pulizia esplicita e deterministica tramite close() mentre impedisce errori di uso dopo la libera che si verificherebbero se il GC finalizzasse il segmento prematuramente. Il design garantisce che la sicurezza della memoria sia mantenuta senza richiedere la sincronizzazione manuale per ogni accesso. Questa scelta architettonica colma il divario tra la gestione manuale della memoria e il paradigma di raccolta automatica dei rifiuti di Java.
Considera un'applicazione di trading ad alta frequenza che elabora dati di mercato tramite MemorySegment mappati a buffer off-heap condivisi con un gateway di scambio C++. Il problema emerge quando più thread tentano di leggere aggiornamenti di prezzo mentre un thread di manutenzione in background aggiorna periodicamente il buffer chiudendo la vecchia Arena e allocando una nuova. Senza una corretta sicurezza temporale, un thread lettore potrebbe tentare di accedere a un segmento la cui memoria sottostante è stata restituita al sistema operativo, causando un crash della JVM o una silenziosa corruzione dei dati.
Una soluzione considerata è stata il conteggio esplicito dei riferimenti con AtomicInteger. Ogni operazione di lettura aumenterebbe il contatore e diminuirebbe dopo il completamento. I pro includono una logica semplice e la rilevazione immediata delle perdite. Tuttavia, i contro comportano una significativa contesa sulla variabile atomica sotto carico elevato, e non si integra con il garbage collector; un decremento dimenticato provoca comunque una perdita di memoria, e non impedisce la chiusura dell'arena mentre il codice nativo detiene un puntatore raw.
Un altro approccio ha coinvolto blocchi di try-with-resources che avvolgono ogni accesso, garantendo che l'arena rimanga aperta durante l'operazione. I pro sono la delimitazione deterministica e una sintassi pulita. I contro includono la chiusura e la riapertura eccessiva delle arene per operazioni brevi, il che è proibitivo quando si allocano migliaia di segmenti al secondo. Inoltre, questo schema non può proteggere contro callback asincroni dal codice nativo che potrebbero sopravvivere all'ambito Java.
La soluzione scelta ha sfruttato Arena.ofShared() con una corretta collocazione di reachabilityFence e controlli di accesso scopi. Limitando la chiusura dell'arena a un thread di manutenzione dedicato e garantendo che tutte le operazioni di lettura validassero la vitalità del segmento prima della dereferenziazione, il sistema ha eliminato le condizioni di gara. Il meccanismo ScopedMemoryAccess ha fornito controlli a costo zero sul percorso veloce mentre le garanzie di raggiungibilità della JVM hanno impedito interferenze del GC. Il risultato è stato un sistema stabile che elabora milioni di messaggi al secondo senza crash nativi o perdite di memoria.
Perché MemorySegment genera WrongThreadException anche quando il segmento non è esplicitamente confinato, e come determina il tipo di Arena le semantiche di confinamento dei thread?
Molti candidati assumono che tutti i segmenti siano thread-safe per impostazione predefinita. In realtà, Arena.ofConfined() crea segmenti accessibili solo dal thread di origine, applicato da controlli di identificazione del thread in ScopedMemoryAccess. Arena.ofShared() consente l'accesso tra thread ma richiede sincronizzazione esterna. L'eccezione si verifica quando l'indirizzo di un segmento confinato viene passato a un altro thread tramite un lambda o un callback.
Qual è la differenza tra chiudere direttamente un MemorySegment rispetto alla chiusura del suo Arena genitore, e perché la chiusura di un'arena invalida tutti i segmenti derivati contemporaneamente?
Una comune concezione errata è che i segmenti siano risorse indipendenti. In effetti, i segmenti derivati tramite slice() o reinterpret() condividono lo stesso ScopedMemoryAccess.Scope della loro arena genitore. Quando viene invocato Arena.close(), invalida l'intero ambito, cascando a tutti i segmenti derivati. Chiudere un singolo segmento segna solo quella vista specifica come non valida, ma la memoria sottostante rimane allocata fino alla chiusura dell'arena.