JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
Предупреждения при использовании методов доступа к памяти в sun.misc.Unsafe
| Автор | Ron Pressler & Alex Buckley |
| Ответственный | Ron Pressler |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 24 |
| Компонент | core-libs |
| Обсуждение | jdk dash dev at openjdk dot org |
| Трудоёмкость | S |
| Длительность | S |
| Связан с | JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal |
| Рецензенты | Alan Bateman |
| Одобрен | Alan Bateman |
| Создан | 2024/10/14 17:42 |
| Обновлён | 2025/03/03 15:21 |
| Задача | 8342077 |
Аннотация
Выдавать предупреждение во время выполнения при первом вызове любого метода доступа к памяти в sun.misc.Unsafe. Все эти неподдерживаемые методы были окончательно объявлены устаревшими в JDK 23. Им на смену пришли стандартные API: VarHandle API (JEP 193, JDK 9) и Foreign Function & Memory API (JEP 454, JDK 22). Мы настоятельно призываем разработчиков библиотек перейти с sun.misc.Unsafe на поддерживаемые замены, чтобы приложения могли без проблем переходить на современные выпуски JDK.
История
Этот JEP продолжает JEP 471 (JDK 23), который объявил методы доступа к памяти в sun.misc.Unsafe устаревшими и подлежащими удалению в одном из будущих выпусков и описал постепенный процесс их удаления. Разделы «Цели», «Что не является целью», «Мотивация» и «Риски и допущения» этого JEP по существу совпадают с соответствующими разделами JEP 471.
Цели
-
Подготовить экосистему к удалению методов доступа к памяти в
sun.misc.Unsafeв одном из будущих выпусков JDK. -
Уведомлять разработчиков, когда их приложения прямо или косвенно используют методы доступа к памяти в
sun.misc.Unsafe.
Что не является целью
- Выдавать предупреждения при использовании любого члена класса
sun.misc.Unsafeне является целью. Небольшое число его методов не предназначено для доступа к памяти; они будут объявлены устаревшими и удалены отдельно.
Мотивация
Класс sun.misc.Unsafe появился в 2002 году как способ выполнять низкоуровневые операции для Java-классов внутри JDK. Большинство его методов — 79 из 87 — предназначены для доступа к памяти: либо к куче JVM, которой управляет сборщик мусора, либо к памяти вне кучи, которую JVM не контролирует. Как следует из названия класса, эти методы доступа к памяти небезопасны: они могут приводить к неопределённому поведению, в том числе к аварийному завершению JVM. Поэтому они не были открыты как стандартный API. Их не предполагалось использовать широким кругом клиентов, и они не задумывались как постоянные. Напротив, их ввели в расчёте на то, что они будут использоваться исключительно внутри JDK, что вызывающий код внутри JDK будет выполнять исчерпывающие проверки безопасности перед их использованием и что со временем в платформу Java будут добавлены безопасные стандартные API для этой функциональности.
Однако в 2002 году не было способа запретить использование sun.misc.Unsafe за пределами JDK, и его методы доступа к памяти стали удобным инструментом для разработчиков библиотек, которым требовались больше возможностей и производительности, чем могли дать стандартные API. Например, sun.misc.Unsafe::compareAndSwap может выполнить операцию CAS (compare-and-swap) над полем без накладных расходов API java.util.concurrent.atomic, а sun.misc.Unsafe::setMemory может работать с памятью вне кучи без ограничения в 2 ГБ, присущего java.nio.ByteBuffer. Библиотеки, которые используют ByteBuffer для работы с памятью вне кучи, например Apache Hadoop и Cassandra, применяют sun.misc.Unsafe::invokeCleaner для повышения эффективности, своевременно освобождая память вне кучи.
К сожалению, не все библиотеки добросовестно выполняют проверки безопасности перед вызовом методов доступа к памяти, поэтому существует риск сбоев и аварийного завершения приложений. Некоторые случаи использования этих методов не нужны и объясняются лёгкостью копирования кода с интернет-форумов. Другие случаи использования этих методов могут вынудить JVM отключить оптимизации, и производительность оказывается хуже, чем при использовании обычных Java-массивов. Тем не менее из-за столь широкого использования методов доступа к памяти класс sun.misc.Unsafe не был инкапсулирован вместе с другими низкоуровневыми API в JDK 9 (JEP 260). Он по-прежнему доступен по умолчанию в JDK 23, пока не появятся безопасные поддерживаемые альтернативы.
За последние несколько лет мы представили два стандартных API, которые служат безопасной и производительной заменой методов доступа к памяти в sun.misc.Unsafe:
-
java.lang.invoke.VarHandle, появившийся в JDK 9 (JEP 193), предоставляет методы для безопасной и эффективной работы с памятью в куче, то есть с полями объектов, статическими полями классов и элементами массивов. -
java.lang.foreign.MemorySegment, появившийся в JDK 22 (JEP 454), предоставляет методы для безопасного и эффективного доступа к памяти вне кучи, иногда совместно сVarHandle.
Эти стандартные API гарантируют отсутствие неопределённого поведения, обещают долгосрочную стабильность и качественно интегрированы с инструментами и документацией платформы Java. (Примеры их использования приведены в JEP 471.) Раз эти API уже доступны, теперь уместно объявить методы доступа к памяти в sun.misc.Unsafe устаревшими и в конечном итоге удалить их.
Удаление методов доступа к памяти в sun.misc.Unsafe — часть долгосрочной скоординированной работы, цель которой — обеспечить целостность по умолчанию платформы Java. К другим инициативам относятся ограничения на Java Native Interface (JNI, JEP 472) и на динамическую загрузку агентов (JEP 451). Эти меры сделают платформу Java более безопасной и более производительной. Они также снизят риск того, что разработчики приложений застрянут на старых выпусках JDK из-за библиотек, которые перестают работать в новых выпусках при изменении неподдерживаемых API.
Описание
Мы объявляем методы доступа к памяти в sun.misc.Unsafe устаревшими и удаляем их поэтапно:
-
В JDK 23 все методы доступа к памяти были объявлены устаревшими и подлежащими удалению. Из-за этого при компиляции кода, ссылающегося на эти методы, выдаются предупреждения об устаревании, которые сообщают разработчикам библиотек о предстоящем удалении методов.
-
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::setMemory has been called by com.foo.bar.Server (file:/tmp/foobarserver/thing.jar) WARNING: Please consider reporting this to the maintainers of com.foo.bar.Server WARNING: sun.misc.Unsafe::setMemory will be removed in a future release -
JDK 26 или более поздний выпуск будет выбрасывать исключение при каждом использовании метода доступа к памяти, напрямую или через рефлексию. Это ещё раз предупредит разработчиков и пользователей приложений о скором удалении методов.
-
В выпусках после JDK 26 мы удалим методы доступа к памяти, у которых есть стандартные замены начиная с JDK 9 (2017).
-
В выпусках после JDK 26 мы удалим методы доступа к памяти, у которых стандартные замены появились только в JDK 22 (2023).
Полный список методов доступа к памяти и их стандартных замен приведён в JEP 471.
Поиск использования методов доступа к памяти в sun.misc.Unsafe
Подавляющее большинство Java-разработчиков не используют sun.misc.Unsafe явно в собственном коде. Однако многие приложения прямо или косвенно зависят от библиотек, которые используют методы доступа к памяти в sun.misc.Unsafe.
В JDK 23 и более поздних выпусках вы можете оценить, как на используемые вами библиотеки влияют объявление этих методов устаревшими и их удаление, запустив приложение с новым параметром командной строки --sun-misc-unsafe-memory-access={allow|warn|debug|deny}. Этот параметр по духу и форме похож на параметр --illegal-access, введённый в JEP 261 в JDK 9. Он работает так:
-
--sun-misc-unsafe-memory-access=allowразрешает использовать методы доступа к памяти без предупреждений во время выполнения.Этот режим был режимом по умолчанию в JDK 23.
-
--sun-misc-unsafe-memory-access=warnразрешает использовать методы доступа к памяти, но выдаёт одно предупреждение, как описано выше.Этот режим будет режимом по умолчанию в JDK 24.
-
--sun-misc-unsafe-memory-access=debugразрешает использовать методы доступа к памяти, но при каждом использовании любого метода доступа к памяти, напрямую или через рефлексию, выдаёт однострочное предупреждение и трассировку стека. -
--sun-misc-unsafe-memory-access=denyзапрещает использовать методы доступа к памяти: при каждом использовании любого такого метода, напрямую или через рефлексию, выбрасываетсяUnsupportedOperationException.
Значение параметра --sun-misc-unsafe-memory-access по умолчанию меняется от выпуска к выпуску по мере прохождения описанных выше этапов:
-
allowбыло значением по умолчанию на этапе 1 (JDK 23). -
warnбудет значением по умолчанию на этапе 2 (JDK 24), как если бы каждый запуск лаунчераjavaвключал--sun-misc-unsafe-memory-access=warn. В JDK 24 можно вернуть значение сwarnнаallowи тем самым избежать предупреждения. То есть лаунчерjavaможно запустить с--sun-misc-unsafe-memory-access=allow. -
denyбудет значением по умолчанию на этапе 3 (JDK 26 или более поздний выпуск). На этапе 3 можно будет вернуть значение сdenyнаwarn, чтобы получать одно предупреждение вместо исключений. Использоватьallow, чтобы избежать предупреждения, будет нельзя. -
На этапе 5, когда все методы доступа к памяти будут удалены,
--sun-misc-unsafe-memory-accessбудет игнорироваться. Со временем этот параметр будет удалён.
Определить, когда используются методы доступа к памяти, можно также с помощью JDK Flight Recorder (JFR). Если JFR включён в командной строке, JVM записывает событие jdk.DeprecatedInvocation при каждом вызове окончательно устаревшего метода. С помощью этого события можно выявить использование методов доступа к памяти в sun.misc.Unsafe. Например, вот как создать запись JFR и затем вывести события jdk.DeprecatedInvocation:
$ java -XX:StartFlightRecording:filename=recording.jfr ...
$ jfr print --events jdk.DeprecatedInvocation recording.jfr
jdk.DeprecatedInvocation {
startTime = 11:53:00.196 (2024-11-08)
method = sun.misc.Unsafe.staticFieldOffset(Field)
invocationTime = 11:53:00.174 (2024-11-08)
forRemoval = true
stackTrace = [
Foo.main(String[]) line: 16
...
]
}
$
Подробнее об этом событии и его ограничениях см. в примечании к выпуску JDK 22.
Риски и допущения
-
За прошедшие годы методы
sun.misc.Unsafe, не связанные с доступом к памяти, объявлялись устаревшими и подлежащими удалению после появления стандартных замен, и многие из них уже удалены:-
sun.misc.Unsafe::defineClassбыл удалён в JDK 11 после появленияjava.lang.invoke.MethodHandles.Lookup::defineClassв JDK 9. -
sun.misc.Unsafe::defineAnonymousClassбыл удалён в JDK 17 после появленияMethodHandles.Lookup::defineHiddenClassв JDK 15. -
sun.misc.Unsafe::{ensureClass,shouldBe}Initializedбыли удалены в JDK 22 после появленияMethodHandles.Lookup::ensureInitializedв JDK 15. -
Шесть различных методов в
sun.misc.Unsafeбыли объявлены устаревшими и подлежащими удалению в JDK 22 после того, как стали доступны стандартные замены.
Удаление этих сравнительно малоизвестных методов почти не затронуло экосистему Java. Однако методы доступа к памяти известны гораздо шире. Это предложение исходит из того, что их удаление затронет библиотеки. Поэтому, чтобы привлечь как можно больше внимания, мы предлагаем объявить их устаревшими и удалить через процесс JEP, а не просто через запрос CSR и примечание к выпуску.
-
-
Это предложение исходит из того, что разработчики библиотек перейдут с неподдерживаемых методов в
sun.misc.Unsafeна поддерживаемые методы вjava.*.Мы самым настоятельным образом рекомендуем разработчикам библиотек не переходить с неподдерживаемых методов в
sun.misc.Unsafeна неподдерживаемые методы, находящиеся в других местах внутри JDK.Разработчики библиотек, которые проигнорируют эту рекомендацию, вынудят своих пользователей запускать приложения с параметрами командной строки
--add-exportsили--add-opens. Это не просто неудобно, но и рискованно: внутреннее устройство JDK может меняться от выпуска к выпуску без предупреждения, что ломает библиотеки, зависящие от него, а вслед за ними и приложения, зависящие от этих библиотек. -
Риск этого предложения в том, что некоторые библиотеки используют методы
sun.misc.Unsafeдля доступа к памяти в куче такими способами, которые невозможно воспроизвести стандартными API, доступными в JDK 23. Например, библиотека может с помощьюUnsafe::objectFieldOffsetполучить смещение поля в объекте, а затем с помощьюUnsafe::putIntзаписать значениеintпо этому смещению, независимо от того, имеет ли поле типint. Стандартный APIVarHandleне может исследовать объекты или работать с ними на таком низком уровне, поскольку обращается к полям по имени и типу, а не по смещению.Сценарии, основанные на смещениях полей, по сути раскрывают или эксплуатируют детали реализации JVM. По нашему мнению, такие сценарии не обязательно поддерживать в стандартном API.
-
Библиотека может использовать
UNSAFE.getInt(array, arrayBase + offset)для доступа к элементам массива в куче без проверки границ. Этот приём обычно применяется для произвольного доступа, поскольку последовательный доступ к элементам массива, будь то через обычные операции индексации массива или через APIMemorySegment, и так выигрывает от устранения проверок границ JIT-компилятором.По нашему мнению, произвольный доступ к элементам массива без проверки границ — не тот сценарий, который нужно поддерживать в стандартном API. Произвольный доступ через операции индексации массива или через API
MemorySegmentнемного проигрывает в производительности методамsun.misc.Unsafeдля доступа к памяти в куче, но даёт большой выигрыш в безопасности и сопровождаемости. В частности, стандартные API гарантированно надёжно работают на всех платформах и во всех выпусках JDK, даже если реализация массивов в JVM в будущем изменится.