openjdk.ruOpenJDK на русском

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 устаревшими и удаляем их поэтапно:

  1. В JDK 23 все методы доступа к памяти были объявлены устаревшими и подлежащими удалению. Из-за этого при компиляции кода, ссылающегося на эти методы, выдаются предупреждения об устаревании, которые сообщают разработчикам библиотек о предстоящем удалении методов.

  2. JDK 24 по умолчанию будет выдавать предупреждение при первом использовании любого метода доступа к памяти, напрямую или через рефлексию. То есть будет выдано не более одного предупреждения, независимо от того, какие методы доступа к памяти используются и сколько раз используется каждый метод. Так разработчики и пользователи приложений узнают о предстоящем удалении методов и о необходимости обновить библиотеки. Пример предупреждения:

    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
  3. JDK 26 или более поздний выпуск будет выбрасывать исключение при каждом использовании метода доступа к памяти, напрямую или через рефлексию. Это ещё раз предупредит разработчиков и пользователей приложений о скором удалении методов.

  4. В выпусках после JDK 26 мы удалим методы доступа к памяти, у которых есть стандартные замены начиная с JDK 9 (2017).

  5. В выпусках после 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, не связанные с доступом к памяти, объявлялись устаревшими и подлежащими удалению после появления стандартных замен, и многие из них уже удалены:

    Удаление этих сравнительно малоизвестных методов почти не затронуло экосистему 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. Стандартный API VarHandle не может исследовать объекты или работать с ними на таком низком уровне, поскольку обращается к полям по имени и типу, а не по смещению.

    Сценарии, основанные на смещениях полей, по сути раскрывают или эксплуатируют детали реализации JVM. По нашему мнению, такие сценарии не обязательно поддерживать в стандартном API.

  • Библиотека может использовать UNSAFE.getInt(array, arrayBase + offset) для доступа к элементам массива в куче без проверки границ. Этот приём обычно применяется для произвольного доступа, поскольку последовательный доступ к элементам массива, будь то через обычные операции индексации массива или через API MemorySegment, и так выигрывает от устранения проверок границ JIT-компилятором.

    По нашему мнению, произвольный доступ к элементам массива без проверки границ — не тот сценарий, который нужно поддерживать в стандартном API. Произвольный доступ через операции индексации массива или через API MemorySegment немного проигрывает в производительности методам sun.misc.Unsafe для доступа к памяти в куче, но даёт большой выигрыш в безопасности и сопровождаемости. В частности, стандартные API гарантированно надёжно работают на всех платформах и во всех выпусках JDK, даже если реализация массивов в JVM в будущем изменится.