L'API Foreign Function & Memory (FFM) introduit MemorySegment pour accéder en toute sécurité à la mémoire hors tas. Chaque segment est associé à une MemorySession (ou Arena dans les versions plus récentes) qui définit son cycle de vie. Lorsque l'arène est fermée, la couche ScopedMemoryAccess marque tous les segments associés comme "non vivants."
Toute tentative d'accès ultérieure déclenche une vérification ScopedMemoryAccess.Scope qui lance immédiatement une IllegalStateException. Pour empêcher le ramasse-miettes de récupérer un segment pendant qu'une opération native est en cours, la JVM utilise des sémantiques de reachabilityFence de manière implicite. Le compilateur insère des barrières de maintien à des frontières critiques, garantissant que l'objet segment reste fortement accessible jusqu'à la fin de l'appel natif.
Cette coordination permet un nettoyage explicite et déterministe via close() tout en empêchant les erreurs d'utilisation après désallocation qui surviendraient si le GC finalisait prématurément le segment. La conception garantit que la sécurité mémoire est maintenue sans nécessiter de synchronisation manuelle pour chaque accès. Ce choix architectural comble le fossé entre la gestion manuelle de la mémoire et le paradigme de collecte automatique des déchets de Java.
Considérez une application de trading haute fréquence traitant des données de marché via MemorySegment mappé à des buffers hors tas partagés avec une passerelle d'échange C++. Le problème se pose lorsque plusieurs threads tentent de lire des mises à jour de prix pendant qu'un thread de maintenance en arrière-plan actualise périodiquement le buffer en fermant l'ancienne Arena et en allouant une nouvelle. Sans sécurité temporelle appropriée, un thread lecteur pourrait tenter d'accéder à un segment dont la mémoire sous-jacente a été retournée au système d'exploitation, provoquant un crash de la JVM ou une corruption silencieuse des données.
Une solution envisagée était le comptage de références explicites avec AtomicInteger. Chaque opération de lecture incrémenterait le compteur et décrémenterait après l'achèvement. Les avantages incluent une logique simple et une détection immédiate des fuites. Cependant, les inconvénients concernent une contention significative sur la variable atomique sous forte charge, et cela ne s'intègre pas avec le ramasse-miettes ; un décrément oublié fuite toujours de la mémoire, et cela n'empêche pas l'arène de se fermer pendant que le code natif détient un pointeur brut.
Une autre approche impliquait des blocs try-with-resources enveloppant chaque accès, garantissant que l'arène reste ouverte pendant l'opération. Les avantages sont un scope déterministe et une syntaxe claire. Les inconvénients incluent une fermeture et une réouverture excessives des arènes pour des opérations à courte durée de vie, ce qui est prohibitivement coûteux lors de l'allocation de milliers de segments par seconde. De plus, ce modèle ne peut pas protéger contre des rappels asynchrones du code natif qui pourraient survivre au scope Java.
La solution choisie a tiré parti de Arena.ofShared() avec un placement approprié de reachabilityFence et des vérifications d'accès contrôlées. En limitant la fermeture de l'arène à un thread de maintenance dédié et en veillant à ce que toutes les opérations de lecture valident la vitalité du segment avant de le déséférencer, le système a éliminé les conditions de concurrence. Le mécanisme ScopedMemoryAccess a fourni des vérifications sans coût sur le chemin rapide tandis que les garanties de portée de la JVM ont empêché toute interférence du GC. Le résultat était un système stable traitant des millions de messages par seconde sans plantages natifs ni fuites de mémoire.
Pourquoi MemorySegment lance-t-il WrongThreadException même lorsque le segment n'est pas explicitement confiné, et comment le type d'Arena détermine-t-il les sémantiques de confinement des threads ?
De nombreux candidats supposent que tous les segments sont thread-safe par défaut. En réalité, Arena.ofConfined() crée des segments accessibles uniquement par le thread d'origine, imposé par des vérifications d'identifiant de thread dans ScopedMemoryAccess. Arena.ofShared() permet un accès entre threads mais nécessite une synchronisation externe. L'exception se produit lorsqu'une adresse de segment confinée est passée à un autre thread via une lambda ou un rappel.
En quoi le mécanisme de reachabilityFence diffère-t-il de PhantomReference lors de l'assurance que les ressources hors tas restent valides pendant les appels natifs ?
Les candidats confondent souvent ces deux mécanismes. PhantomReference permet un nettoyage post-mortem après qu'un objet est devenu injoignable, ce qui est trop tard pour prévenir une utilisation après désallocation pendant une opération active. reachabilityFence agit comme une barrière insérée par le compilateur qui maintient l'objet fortement accessible jusqu'à l'exécution de la barrière. Dans FFM, la JVM insère automatiquement ces barrières autour des accesseurs de MemorySegment, garantissant que le segment reste vivant tout au long de l'accès mémoire natif sans nécessiter de placement manuel dans le code utilisateur.
Quelle est la distinction entre la fermeture d'un MemorySegment directement par rapport à la fermeture de son Arena parent, et pourquoi la fermeture d'une arène invalide-t-elle simultanément tous les segments dérivés ?
Une idée reçue commune est que les segments sont des ressources indépendantes. En réalité, les segments dérivés via slice() ou reinterpret() partagent le même ScopedMemoryAccess.Scope que leur arène parente. Lorsque Arena.close() est invoqué, il invalide l'ensemble du scope, se répercutant sur tous les segments dérivés. La fermeture d'un segment individuel ne marque que cette vue spécifique comme invalide, mais la mémoire sous-jacente reste allouée jusqu'à la fermeture de l'arène.