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

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:

  1. Пометить все методы доступа к памяти — 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

    Эти предупреждения будут выдаваться и дальше, и подавить их нельзя.

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

  3. Выбрасывать исключение, когда используется метод доступа к памяти, напрямую или через рефлексию. Так разработчики и пользователи приложений ещё раз будут предупреждены о скором удалении методов.

  4. Удалить методы on-heap. Эти методы будут удалены первыми, потому что у них есть стандартные замены начиная с JDK 9, вышедшего в 2017 году.

  5. Удалить методы 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_OFFSET
  • int ARRAY_[TYPE]_BASE_OFFSET
  • int ARRAY_[TYPE]_INDEX_SCALE

Методы off-heap

Бимодальные методы доступа к памяти

Примеры перехода

Доступ к памяти в куче

Предположим, у класса 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, и многие из них уже удалены:

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

Дальнейшая работа

После того как 79 методов доступа к памяти будут помечены как Deprecated for Removal, в sun.misc.Unsafe останутся только три метода, не помеченные как Deprecated (устаревший):

  • pageSize, который будет помечен как Deprecated и удалён отдельно. Разработчикам библиотек рекомендуется получать размер страницы памяти непосредственно от ОС через downcall.

  • throwException, который будет помечен как Deprecated и удалён отдельно. Исторически этот метод использовался методами JDK, чтобы оборачивать проверяемые исключения в непроверяемые, но эти методы, например Class::newInstance, теперь помечены как Deprecated.

  • allocateInstance, который в среднесрочной перспективе останется единственным методом в sun.misc.Unsafe. Некоторые библиотеки сериализации используют его для десериализации. Создание стандартной замены — долгосрочный проект.