JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
Пометка методов доступа к памяти в sun.misc.Unsafe как Deprecated for Removal (устаревший, будет удалён)
| Автор | Ron Pressler & Alex Buckley |
| Ответственный | Ron Pressler |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 23 |
| Компонент | core-libs |
| Обсуждение | jdk dash dev at openjdk dot org |
| Связан с | JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe |
| Рецензенты | Alan Bateman, Brian Goetz, Mark Reinhold, Maurizio Cimadamore, Paul Sandoz |
| Одобрен | Alan Bateman |
| Создан | 2024/01/05 15:46 |
| Обновлён | 2026/01/09 21:37 |
| Задача | 8323072 |
Аннотация
Пометить методы доступа к памяти в sun.misc.Unsafe как Deprecated for Removal, чтобы удалить их в одном из будущих выпусков. Эти неподдерживаемые методы заменены стандартными API, а именно VarHandle API (JEP 193, JDK 9) и Foreign Function & Memory API (JEP 454, JDK 22). Мы настоятельно рекомендуем разработчикам библиотек перейти с sun.misc.Unsafe на поддерживаемые замены, чтобы приложения могли без проблем переходить на современные выпуски JDK.
Цели
-
Подготовить экосистему к удалению методов доступа к памяти в
sun.misc.Unsafeв одном из будущих выпусков JDK. -
Помочь разработчикам понять, когда их приложения прямо или косвенно зависят от методов доступа к памяти в
sun.misc.Unsafe.
Что не является целью
-
Удалять класс
sun.misc.Unsafeцеликом не является целью. Небольшая часть его методов не используется для доступа к памяти; они будут помечены как устаревшие и удалены отдельно. -
Изменять другие классы
sun.*в модулеjdk.unsupportedне является целью.
Мотивация
Класс 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 22, пока не появятся безопасные поддерживаемые альтернативы.
За последние несколько лет мы добавили два стандартных API, которые служат безопасной и производительной заменой методов доступа к памяти в sun.misc.Unsafe:
-
java.lang.invoke.VarHandle, появившийся в JDK 9 (JEP 193), предоставляет методы для безопасной и эффективной работы с памятью в куче, то есть с полями объектов, статическими полями классов и элементами массивов. -
java.lang.foreign.MemorySegment, появившийся в JDK 22 (JEP 454), предоставляет методы для безопасного и эффективного доступа к памяти вне кучи, иногда совместно сVarHandle.
Эти стандартные API гарантируют отсутствие неопределённого поведения, обещают долгосрочную стабильность и хорошо интегрированы с инструментами и документацией платформы Java (примеры их использования приведены ниже). Раз эти API доступны, теперь уместно пометить методы доступа к памяти в sun.misc.Unsafe как устаревшие и в итоге удалить их.
Удаление методов доступа к памяти в sun.misc.Unsafe — часть долгосрочной скоординированной работы, цель которой — обеспечить целостность по умолчанию для платформы Java. К другим инициативам относятся ограничения на Java Native Interface (JNI, JEP 472) и на динамическую загрузку агентов (JEP 451). Эта работа сделает платформу Java более безопасной и более производительной. Она также снизит риск того, что разработчики приложений застрянут на старых выпусках JDK из-за библиотек, которые перестают работать на новых выпусках при изменении неподдерживаемых API.
Описание
Методы доступа к памяти sun.misc.Unsafe можно разделить на три категории:
- методы доступа к памяти в куче (on-heap),
- методы доступа к памяти вне кучи (off-heap) и
- методы доступа как к памяти в куче, так и к памяти вне кучи (bimodal — бимодальный метод принимает параметр, который либо ссылается на объект в куче, либо равен
null, что означает доступ к памяти вне кучи).
Мы будем помечать методы как устаревшие и удалять их поэтапно, причём каждый этап приходится на отдельный Feature Release (функциональный выпуск) JDK:
-
Пометить все методы доступа к памяти — on-heap, off-heap и bimodal — как Deprecated for Removal. Из-за этого код, который обращается к этим методам, будет получать при компиляции предупреждения об устаревании, и разработчики библиотек узнают о предстоящем удалении методов. Новый параметр командной строки, описанный ниже, позволит разработчикам и пользователям приложений получать предупреждения во время выполнения, когда эти методы используются.
Помимо предупреждений об устаревании,
javacвыдаёт предупреждения об использованииsun.misc.Unsafeс 2006 года:warning: Unsafe is internal proprietary API and may be removed in a future releaseЭти предупреждения будут выдаваться и дальше, и подавить их нельзя.
-
Выдавать предупреждение во время выполнения, как описано ниже, когда используется метод доступа к памяти, напрямую или через рефлексию. Так разработчики и пользователи приложений узнают о предстоящем удалении методов и о необходимости обновить библиотеки.
-
Выбрасывать исключение, когда используется метод доступа к памяти, напрямую или через рефлексию. Так разработчики и пользователи приложений ещё раз будут предупреждены о скором удалении методов.
-
Удалить методы on-heap. Эти методы будут удалены первыми, потому что у них есть стандартные замены начиная с JDK 9, вышедшего в 2017 году.
-
Удалить методы off-heap и bimodal. Эти методы будут удалены позже, потому что стандартные замены у них появились только в JDK 22 в 2023 году.
По срокам мы планируем реализовать
- этап 1 в рамках этого JEP в JDK 23;
- этап 2, с выдачей предупреждений во время выполнения, в JDK 25 или раньше;
- этап 3, с выбрасыванием исключений по умолчанию, в JDK 26 или позже; и
- этапы 4 и 5, на которых методы будут удалены, в выпусках после JDK 26.
Когда придёт время, мы можем реализовать этапы 4 и 5 одновременно, если это будет уместно.
Разрешение использовать методы доступа к памяти в sun.misc.Unsafe
Подавляющее большинство разработчиков на Java не используют sun.misc.Unsafe явно в своём коде. Однако многие приложения прямо или косвенно зависят от библиотек, которые используют методы доступа к памяти sun.misc.Unsafe. Начиная с JDK 23 разработчики приложений могут оценить, как пометка этих методов как устаревших и их удаление повлияют на библиотеки, запустив приложение с новым параметром командной строки --sun-misc-unsafe-memory-access={allow|warn|debug|deny}. Этот параметр по духу и по форме похож на параметр --illegal-access, добавленный в JDK 9 в JEP 261. Он работает так:
-
--sun-misc-unsafe-memory-access=allowразрешает использовать методы доступа к памяти без предупреждений во время выполнения. -
--sun-misc-unsafe-memory-access=warnразрешает использовать методы доступа к памяти, но выдаёт предупреждение при первом использовании любого метода доступа к памяти, напрямую или через рефлексию. То есть выдаётся не более одного предупреждения, независимо от того, какие методы доступа к памяти используются и сколько раз используется каждый метод. -
--sun-misc-unsafe-memory-access=debugразрешает использовать методы доступа к памяти, но выдаёт однострочное предупреждение и трассировку стека при каждом использовании любого метода доступа к памяти, напрямую или через рефлексию. -
--sun-misc-unsafe-memory-access=denyзапрещает использовать методы доступа к памяти, выбрасываяUnsupportedOperationExceptionпри каждом использовании любого такого метода, напрямую или через рефлексию.
Пример предупреждений, которые включает значение параметра warn:
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
Значение параметра --sun-misc-unsafe-memory-access по умолчанию будет меняться от выпуска к выпуску по мере прохождения описанных выше этапов:
-
allow— значение по умолчанию на этапе 1, как если бы при каждом вызове средства запускаjavaуказывался--sun-misc-unsafe-memory-access=allow. -
warnбудет значением по умолчанию на этапе 2. На этапе 2 можно будет вернуть значение сwarnнаallowи тем самым избежать предупреждения. -
denyбудет значением по умолчанию на этапе 3. На этапе 3 можно будет вернуть значение сdenyнаwarn, чтобы получать предупреждение вместо исключений. Использоватьallow, чтобы избежать предупреждения, будет нельзя. -
На этапе 5, когда все методы доступа к памяти будут удалены,
--sun-misc-unsafe-memory-accessбудет игнорироваться. Со временем этот параметр будет удалён.
Следующие инструменты JDK могут помочь опытным разработчикам понять, как их код использует устаревшие методы в sun.misc.Unsafe:
-
javacвыдаёт предупреждение об устаревании каждый раз, когда исходный код использует метод доступа к памяти вsun.misc.Unsafe. Каждое предупреждение об устаревании можно подавить в коде с помощью@SuppressWarnings("removal"). -
Если JDK Flight Recorder (JFR) включён в командной строке, событие
jdk.DeprecatedInvocationзаписывается при каждом вызове метода, помеченного как Deprecated for Removal. С помощью этого события можно выявить использование методов доступа к памяти вsun.misc.Unsafe.Например, вот как создать запись JFR, а затем вывести события
jdk.DeprecatedInvocation:$ java -XX:StartFlightRecording:filename=recording.jfr ... $ jfr print --events jdk.DeprecatedInvocation recording.jfrЭто событие не записывается, если JFR запускается во время выполнения с помощью
jcmdили JFR API. Подробнее об этом событии и его ограничениях см. в соответствующем примечании к выпуску JDK 22.
Методы доступа к памяти sun.misc.Unsafe и их замены
Методы on-heap
long objectFieldOffset(Field f)long staticFieldOffset(Field f)Object staticFieldBase(Field f)int arrayBaseOffset(Class<?> arrayClass)int arrayIndexScale(Class<?> arrayClass)
Эти методы нужны для получения смещений или масштабов, которые затем используются с бимодальными методами (см. ниже) для чтения и записи полей или элементов массивов. Теперь эти сценарии покрываются VarHandle и MemorySegment::ofArray.
В редких случаях эти методы используются сами по себе, чтобы исследовать физическое размещение объектов в памяти и управлять им (см. примеры здесь). Поддерживаемой замены для этого сценария нет; подробнее см. ниже.
Первые три из перечисленных выше методов были помечены как устаревшие в JDK 18.
Поля, связанные с этими методами, также помечены как Deprecated for Removal:
int INVALID_FIELD_OFFSETint ARRAY_[TYPE]_BASE_OFFSETint ARRAY_[TYPE]_INDEX_SCALE
Методы off-heap
-
long allocateMemory(long bytes)— заменяется наArena::allocateили на вызов через FFM (downcall) функцииmalloc()библиотеки C -
long reallocateMemory(long address, long bytes)— вызов (downcall)realloc() -
void freeMemory(long address)—Arena::closeили вызов (downcall)free() -
void invokeCleaner(java.nio.ByteBuffer directBuffer)—MemorySegment::asByteBuffer -
void setMemory(long address, long bytes, byte value)—MemorySegment::fill -
void copyMemory(long srcAddress, long destAddress, long bytes)—MemorySegment::copy -
[type] get[Type](long address)—MemorySegment.get(ValueLayout.Of[Type] layout, long offset) -
void put[Type](long address, [type] x)—MemorySegment.set(ValueLayout.of[Type] layout, long offset, [type] value) -
long getAddress(long address)—MemorySegment.get(ValueLayout.OfAddress layout, long offset) -
void putAddress(long address, long x)—MemorySegment.set(ValueLayout.ofAddress layout, long offset, MemorySegment value) -
int addressSize()(иint ADDRESS_SIZE) —ValueLayout.ADDRESS.byteSize()
Бимодальные методы доступа к памяти
-
[type] get[Type](Object o, long offset)— заменяется наVarHandle::get -
void put[Type](Object o, long offset, [type] x)—VarHandle::set -
[type] get[Type]Volatile(Object o, long offset)—VarHandle::getVolatile -
void put[Type]Volatile(Object o, long offset, [type] x)—VarHandle::setVolatile -
void putOrdered[Type](Object o, long offset, [type] x)—VarHandle::setRelease -
[type] getAndAdd[Type](Object o, long offset, [type] delta)—VarHandle::getAndAdd -
[type] getAndSet[Type](Object o, long offset, [type] newValue)—VarHandle::getAndSet -
boolean compareAndSwap[Type](Object o, long offset, [type] expected, [type] x)—VarHandle::compareAndSet -
void setMemory(Object o, long offset, long bytes, byte value)—MemorySegment::fillилиArrays::fill -
void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes)—MemorySegment::copyилиSystem::arrayCopy
Примеры перехода
Доступ к памяти в куче
Предположим, у класса Foo есть поле int, значение которого мы хотим атомарно удвоить. Это можно сделать с помощью sun.misc.Unsafe:
class Foo {
private static final Unsafe UNSAFE = ...; // A sun.misc.Unsafe object
private static final long X_OFFSET;
static {
try {
X_OFFSET = UNSAFE.objectFieldOffset(Foo.class.getDeclaredField("x"));
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryToDoubleAtomically() {
int oldValue = x;
return UNSAFE.compareAndSwapInt(this, X_OFFSET, oldValue, oldValue * 2);
}
}
Мы можем улучшить этот код, перейдя на стандартный API VarHandle:
class Foo {
private static final VarHandle X_VH;
static {
try {
X_VH = MethodHandles.lookup().findVarHandle(Foo.class, "x", int.class);
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryAtomicallyDoubleX() {
int oldValue = x;
return X_VH.compareAndSet(this, oldValue, oldValue * 2);
}
}
С помощью sun.misc.Unsafe можно выполнить volatile-запись элемента массива:
class Foo {
private static final Unsafe UNSAFE = ...;
private static final int ARRAY_BASE = UNSAFE.arrayBaseOffset(int[].class);
private static final int ARRAY_SCALE = UNSAFE.arrayIndexScale(int[].class);
private int[] a = new int[10];
public void setVolatile(int index, int value) {
if (index < 0 || index >= a.length)
throw new ArrayIndexOutOfBoundsException(index);
UNSAFE.putIntVolatile(a, ARRAY_BASE + ARRAY_SCALE * index, value);
}
}
Мы можем улучшить этот код, перейдя на VarHandle:
class Foo {
private static final VarHandle AVH = MethodHandles.arrayElementVarHandle(int[].class);
private int[] a = new int[10];
public void setVolatile(int index, int value) {
AVH.setVolatile(a, index, value);
}
}
Доступ к памяти вне кучи
Вот класс, который с помощью sun.misc.Unsafe выделяет буфер вне кучи и выполняет три операции: volatile-запись значения int, массовую инициализацию части буфера и копирование данных буфера в Java-массив int:
class OffHeapIntBuffer {
private static final Unsafe UNSAFE = ...;
private static final int ARRAY_BASE = UNSAFE.arrayBaseOffset(int[].class);
private static final int ARRAY_SCALE = UNSAFE.arrayIndexScale(int[].class);
private final long size;
private long bufferPtr;
public OffHeapIntBuffer(long size) {
this.size = size;
this.bufferPtr = UNSAFE.allocateMemory(size * ARRAY_SCALE);
}
public void deallocate() {
if (bufferPtr == 0) return;
UNSAFE.freeMemory(bufferPtr);
bufferPtr = 0;
}
private boolean checkBounds(long index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException(index);
return true;
}
public void setVolatile(long index, int value) {
checkBounds(index);
UNSAFE.putIntVolatile(null, bufferPtr + ARRAY_SCALE * index, value);
}
public void initialize(long start, long n) {
checkBounds(start);
checkBounds(start + n-1);
UNSAFE.setMemory(bufferPtr + start * ARRAY_SCALE, n * ARRAY_SCALE, 0);
}
public int[] copyToNewArray(long start, int n) {
checkBounds(start);
checkBounds(start + n-1);
int[] a = new int[n];
UNSAFE.copyMemory(null, bufferPtr + start * ARRAY_SCALE, a, ARRAY_BASE, n * ARRAY_SCALE);
return a;
}
}
Мы можем улучшить этот код, перейдя на стандартные API Arena и MemorySegment:
class OffHeapIntBuffer {
private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();
private final Arena arena;
private final MemorySegment buffer;
public OffHeapIntBuffer(long size) {
this.arena = Arena.ofShared();
this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
}
public void deallocate() {
arena.close();
}
public void setVolatile(long index, int value) {
ELEM_VH.setVolatile(buffer, 0L, index, value);
}
public void initialize(long start, long n) {
buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.fill((byte) 0);
}
public int[] copyToNewArray(long start, int n) {
return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.toArray(ValueLayout.JAVA_INT);
}
}
Риски и допущения
-
За прошедшие годы методы
sun.misc.Unsafe, не связанные с доступом к памяти, после появления стандартных замен были помечены как Deprecated for Removal, и многие из них уже удалены:-
sun.misc.Unsafe::defineClassбыл удалён в JDK 11 после того, как в JDK 9 появилсяjava.lang.invoke.MethodHandles.Lookup::defineClass. -
sun.misc.Unsafe::defineAnonymousClassбыл удалён в JDK 17 после того, какMethodHandles.Lookup::defineHiddenClassпоявился в JDK 15. -
sun.misc.Unsafe::{ensureClass,shouldBe}Initializedбыли удалены в JDK 22 после того, как в JDK 15 появилсяMethodHandles.Lookup::ensureInitialized. -
Шесть разных методов
sun.misc.Unsafeбыли помечены как Deprecated for Removal в 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 в будущем изменится.
Дальнейшая работа
После того как 79 методов доступа к памяти будут помечены как Deprecated for Removal, в sun.misc.Unsafe останутся только три метода, не помеченные как Deprecated (устаревший):
-
pageSize, который будет помечен как Deprecated и удалён отдельно. Разработчикам библиотек рекомендуется получать размер страницы памяти непосредственно от ОС через downcall. -
throwException, который будет помечен как Deprecated и удалён отдельно. Исторически этот метод использовался методами JDK, чтобы оборачивать проверяемые исключения в непроверяемые, но эти методы, напримерClass::newInstance, теперь помечены как Deprecated. -
allocateInstance, который в среднесрочной перспективе останется единственным методом вsun.misc.Unsafe. Некоторые библиотеки сериализации используют его для десериализации. Создание стандартной замены — долгосрочный проект.