API anonimowych funkcji i pamięci (FFM) wprowadza MemorySegment do bezpiecznego dostępu do pamięci poza stosami. Każdy segment jest związany z MemorySession (lub Arena w nowszych wersjach), która definiuje jego cykl życia. Gdy arena jest zamykana, warstwa ScopedMemoryAccess oznacza wszystkie powiązane segmenty jako "nieżywe."
Jakiekolwiek dalsze próby dostępu uruchamiają sprawdzenie ScopedMemoryAccess.Scope, które natychmiast rzuca IllegalStateException. Aby zapobiec zebraniu segmentu przez zbieracz śmieci, dopóki operacja natywna jest w toku, JVM stosuje semantykę reachabilityFence implicitnie. Kompilator wstawia bariery keep-alive na krytycznych granicach, zapewniając, że obiekt segmentu pozostaje mocno osiągalny, dopóki wywołanie natywne się nie zakończy.
Ta koordynacja umożliwia expliczne deterministyczne sprzątanie za pomocą close() przy jednoczesnym zapobieganiu błędom użycia po zwolnieniu, które mogłyby wystąpić, gdyby GC zfinalizował segment przed czasem. Projekt zapewnia, że bezpieczeństwo pamięci jest utrzymywane bez konieczności manualnej synchronizacji dla każdego dostępu. Ten architektoniczny wybór łączy manualne zarządzanie pamięcią z automatycznym zbieraniem śmieci w Javie.
Rozważ aplikację do handlu o wysokiej częstotliwości przetwarzającą dane rynkowe za pośrednictwem MemorySegment mapowanego na bufory poza stosami dzielone z bramą C++. Problem pojawia się, gdy wiele wątków próbuje odczytać aktualizacje cen, podczas gdy wątek utrzymania w tle okresowo odświeża bufor, zamykając starą Arena i alokując nową. Bez odpowiedniego bezpieczeństwa czasowego wątek odczytu może próbować uzyskać dostęp do segmentu, którego pamięć bazowa została zwrócona do systemu operacyjnego, co może spowodować awarię JVM lub cichą korupcję danych.
Jednym z rozważanych rozwiązań było ręczne zliczanie odnośników przy użyciu AtomicInteger. Każda operacja odczytu zwiększałaby licznik i zmniejszała go po zakończeniu. Zaletami są prostota logiki i natychmiastowe wykrywanie wycieków. Jednak wady dotyczą znacznego obciążenia zmiennej atomowej przy dużym obciążeniu, a także nieintegracji z automatycznym zbieraniem śmieci; zapomniane zmniejszenie nadal prowadzi do wycieków pamięci, a to nie zapobiega zamykaniu areny, gdy natywny kod trzyma surowy wskaźnik.
Inne podejście polegało na blokach try-with-resources obejmujących każdy dostęp, zapewniając, że arena pozostaje otwarta podczas operacji. Zaletami są deterministyczne ograniczenia i czysta składnia. Wady to nadmierne zamykanie i ponowne otwieranie aren dla krótkotrwałych operacji, co jest nieproporcjonalnie kosztowne przy alokacji tysięcy segmentów na sekundę. Co więcej, ten wzorzec nie może chronić przed asynchronicznymi wywołaniami zwrotnymi z natywnego kodu, które mogą przetrwać zasięg Javy.
Wybrane rozwiązanie polegało na wykorzystaniu Arena.ofShared() z odpowiednim umiejscowieniem reachabilityFence oraz sprawdzeniami dostępu w danym zakresie. Ograniczając zamknięcie areny do dedykowanego wątku konserwacyjnego i zapewniając, że wszystkie operacje odczytu weryfikowały żywotność segmentu przed dereferencją, system wyeliminował warunki wyścigu. Mechanizm ScopedMemoryAccess zapewnił zerowe koszty sprawdzeń na szybkim szlaku, podczas gdy gwarancje osiągalności JVM zapobiegały zakłóceniom ze strony GC. Wynikiem był stabilny system przetwarzający miliony wiadomości na sekundę bez natywnych awarii czy wycieków pamięci.
Dlaczego MemorySegment rzuca WrongThreadException, nawet gdy segment nie jest explicytnie ograniczony, i jak typ Arena określa semantykę ograniczenia wątkowego?
Wielu kandydatów zakłada, że wszystkie segmenty są domyślnie bezpieczne dla wątków. W rzeczywistości Arena.ofConfined() tworzy segmenty dostępne tylko przez wątek pochodzący, egzekwowane przez kontrole identyfikatora wątku w ScopedMemoryAccess. Arena.ofShared() pozwala na dostęp między wątkami, ale wymaga zewnętrznej synchronizacji. Wyjątek występuje, gdy adres segmentu ograniczonego jest przekazywany innemu wątkowi za pomocą lambdy lub wywołania zwrotnego.
Jak mechanizm reachabilityFence różni się od PhantomReference, zapewniając że zasoby poza stosami pozostają ważne podczas wywołań natywnych?
Kandydaci często mylą te dwa mechanizmy. PhantomReference pozwala na sprzątanie pośmiertne po tym, jak obiekt staje się nieosiągalny, co jest za późno do zapobiegania użyciu po zwolnieniu podczas aktywnej operacji. reachabilityFence działa jako bariera wstawiana przez kompilator, która utrzymuje obiekt w silnej dostępności, dopóki ogrodzenie nie zostanie wykonane. W FFM JVM automatycznie wstawia te ogrodzenia wokół akcesorów MemorySegment, zapewniając, że segment pozostaje żywy przez cały czas dostępu do pamięci natywnej bez potrzeby manualnego umieszczania w kodzie użytkownika.
Jaka jest różnica między bezpośrednim zamykaniem MemorySegment a zamykaniem jego rodzica Arena, i dlaczego zamknięcie areny unieważnia wszystkie pochodne segmenty jednocześnie?
Powszechnym nieporozumieniem jest to, że segmenty są niezależnymi zasobami. W rzeczywistości segmenty uzyskane za pomocą slice() lub reinterpret() dzielą tę samą ScopedMemoryAccess.Scope co ich rodzic arena. Gdy wywoływana jest Arena.close(), unieważnia cały zakres, co wpływa na wszystkie pochodne segmenty. Zamknięcie indywidualnego segmentu oznacza tylko, że ten konkretny widok jest unieważniony, ale podstawa pamięci pozostaje alokowana do momentu zamknięcia areny.