Рубрики
Без рубрики

API доступа к внешней памяти в Java 14

Java 14 вводит новый API для обертывания доступа к памяти, управляемой за пределами JVM. Здесь мы исследуем потребность в API и то, как он работает.

Автор оригинала: baeldung.

1. Обзор

Объекты Java находятся в куче. Однако иногда это может привести к таким проблемам, как неэффективное использование памяти, низкая производительность и проблемы с сборкой мусора . Собственная память может быть более эффективной в этих случаях, но ее использование традиционно было очень сложным и подверженным ошибкам.

Java 14 вводит API доступа к внешней памяти для более безопасного и эффективного доступа к собственной памяти.

В этом уроке мы рассмотрим этот API.

2. Мотивация

Эффективное использование памяти всегда было сложной задачей. Это в основном связано с такими факторами, как недостаточное понимание памяти, ее организации и сложных методов адресации памяти.

Например, неправильно реализованный кэш памяти может привести к частой сборке мусора. Это резко снизит производительность приложения.

До появления API доступа к внешней памяти в Java существовало два основных способа доступа к собственной памяти в Java. Это java.nio.ByteBuffer и sun.misc.Unsafe классы.

Давайте быстро рассмотрим преимущества и недостатки этих API.

2.1. API ByteBuffer

ByteBuffer API позволяет создавать прямые байтовые буферы вне кучи . К этим буферам можно получить прямой доступ из программы Java. Однако существуют некоторые ограничения:

  • Размер буфера не может превышать двух гигабайт
  • Сборщик мусора отвечает за освобождение памяти

Кроме того, неправильное использование ByteBuffer может привести к утечке памяти и OutOfMemory ошибкам. Это связано с тем, что неиспользуемая ссылка на память может помешать сборщику мусора освободить память.

2.2. Небезопасный API

Небезопасный API чрезвычайно эффективен благодаря своей модели адресации. Однако, как следует из названия, этот API небезопасен и имеет несколько недостатков:

  • Это часто позволяет программам Java аварийно завершать работу JVM из-за незаконного использования памяти
  • Это нестандартный Java API

2.3. Необходимость в новом API

Таким образом, доступ к чужой памяти ставит перед нами дилемму. Должны ли мы использовать безопасный, но ограниченный путь ( ByteBuffer )? Или мы должны рисковать, используя неподдерживаемый и опасный Небезопасный API?

Новый API доступа к внешней памяти направлен на решение этих проблем.

3. API внешней памяти

API доступа к внешней памяти предоставляет поддерживаемый, безопасный и эффективный API для доступа как к куче, так и к собственной памяти. Он построен на трех основных абстракциях:

  • MemorySegment – моделирует непрерывную область памяти
  • MemoryAddress – местоположение в сегменте памяти
  • MemoryLayout – способ определения компоновки сегмента памяти в нейтральном к языку виде

Давайте обсудим это подробно.

3.1. Сегмент Памяти

Сегмент памяти – это непрерывная область памяти. Это может быть как кучная, так и не кучная память. И есть несколько способов получить сегмент памяти.

Сегмент памяти, поддерживаемый собственной памятью, известен как собственный сегмент памяти. Он создан с использованием одного из перегруженных выделенных собственных методов.

Давайте создадим собственный сегмент памяти объемом 200 байт:

MemorySegment memorySegment = MemorySegment.allocateNative(200);

Сегмент памяти также может быть поддержан существующим массивом Java, выделенным в куче. Например, мы можем создать сегмент памяти массива из массива long :

MemorySegment memorySegment = MemorySegment.ofArray(new long[100]);

Кроме того, сегмент памяти может быть поддержан существующим Java ByteBuffer . Это называется сегментом буферной памяти :

MemorySegment memorySegment = MemorySegment.ofByteBuffer(ByteBuffer.allocateDirect(200));

В качестве альтернативы мы можем использовать файл, сопоставленный с памятью. Это называется сегментом сопоставленной памяти. Давайте определим 200-байтовый сегмент памяти, используя путь к файлу с доступом для чтения и записи:

MemorySegment memorySegment = MemorySegment.mapFromPath(
  Path.of("/tmp/memory.txt"), 200, FileChannel.MapMode.READ_WRITE);

Сегмент памяти присоединяется к определенному потоку . Таким образом, если какой-либо другой поток требует доступа к сегменту памяти, он должен получить доступ с помощью метода acquire .

Кроме того, сегмент памяти имеет пространственные и временные границы с точки зрения доступа к памяти:

  • Пространственная граница — сегмент памяти имеет нижний и верхний пределы
  • Временная граница — управляет созданием, использованием и закрытием сегмента памяти

В совокупности пространственные и временные проверки обеспечивают безопасность СП.

3.2. Адрес памяти

Адрес памяти – это смещение в сегменте памяти . Обычно он получается с помощью метода базовый адрес :

MemoryAddress address = MemorySegment.allocateNative(100).baseAddress();

Адрес памяти используется для выполнения таких операций, как извлечение данных из памяти в базовом сегменте памяти.

3.3. Выделение памяти

Класс Memory Layout позволяет нам описать содержимое сегмента памяти. В частности, он позволяет нам определить, как память разбивается на элементы, где указан размер каждого элемента.

Это немного похоже на описание компоновки памяти как конкретного типа, но без предоставления класса Java. Это похоже на то, как языки, подобные C++, отображают свои структуры в память.

Давайте возьмем пример декартовой координатной точки, определенной с координатами x и y :

int numberOfPoints = 10;
MemoryLayout pointLayout = MemoryLayout.ofStruct(
  MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN).withName("x"),
  MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN).withName("y")
);
SequenceLayout pointsLayout = 
  MemoryLayout.ofSequence(numberOfPoints, pointLayout);

Здесь мы определили макет, состоящий из двух 32-разрядных значений с именами x и y . Этот макет можно использовать с SequenceLayout , чтобы сделать что-то похожее на массив, в данном случае с 10 индексами.

4. Использование Собственной Памяти

4.1. Дескрипторы памяти

Класс Memory Handles позволяет нам создавать дескрипторы Var. A Var-дескриптор разрешает доступ к сегменту памяти.

Давайте попробуем это:

long value = 10;
MemoryAddress memoryAddress = MemorySegment.allocateNative(8).baseAddress();
VarHandle varHandle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());
varHandle.set(memoryAddress, value);
 
assertThat(varHandle.get(memoryAddress), is(value));

В приведенном выше примере мы создаем сегмент памяти из восьми байтов. Нам нужно восемь байтов, чтобы представить длинное число в памяти. Затем мы используем дескриптор Var для его хранения и извлечения.

4.2. Использование дескрипторов памяти со смещением

Мы также можем использовать смещение в сочетании с адресом памяти для доступа к сегменту памяти. Это похоже на использование индекса для получения элемента из массива:

VarHandle varHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
try (MemorySegment memorySegment = MemorySegment.allocateNative(100)) {
    MemoryAddress base = memorySegment.baseAddress();
    for(int i=0; i<25; i++) {
        varHandle.set(base.addOffset((i*4)), i);
    }
    for(int i=0; i<25; i++) {
        assertThat(varHandle.get(base.addOffset((i*4))), is(i));
    }
}

В приведенном выше примере мы храним целые числа от 0 до 24 в сегменте памяти.

Сначала мы создаем Сегмент памяти размером 100 байт. Это происходит потому, что в Java каждое целое число потребляет 4 байта. Поэтому для хранения 25 целочисленных значений нам нужно 100 байт (4*25).

Чтобы получить доступ к каждому индексу, мы устанавливаем дескриптор var для указания на правое смещение с помощью add Offset на базовом адресе.

4.3. Схемы памяти

Класс Memory Layouts определяет различные полезные константы компоновки .

Например, в предыдущем примере мы создали макет Последовательности :

SequenceLayout sequenceLayout = MemoryLayout.ofSequence(25, 
  MemoryLayout.ofValueBits(64, ByteOrder.nativeOrder()));

Это можно выразить более просто, используя константу JAVA_LONG :

SequenceLayout sequenceLayout = MemoryLayout.ofSequence(25, MemoryLayouts.JAVA_LONG);

4.4. Компоновка значений

/| Компоновка значений моделирует компоновку памяти для основных типов данных, таких как целочисленные и плавающие типы. Каждый макет значения имеет размер и порядок байтов. Мы можем создать макет значения , используя метод битов значений :

ValueLayout valueLayout = MemoryLayout.ofValueBits(32, ByteOrder.nativeOrder());

4.5. Расположение последовательностей

A Макет последовательности обозначает повторение заданного макета. Другими словами, это можно рассматривать как последовательность элементов, аналогичную массиву с определенным расположением элементов.

Например, мы можем создать макет последовательности для 25 элементов по 64 бита каждый:

SequenceLayout sequenceLayout = MemoryLayout.ofSequence(25, 
  MemoryLayout.ofValueBits(64, ByteOrder.nativeOrder()));

4.6. Групповая игра

A Groupplayout может объединять несколько макетов элементов . Макеты элементов могут быть либо аналогичными типами, либо комбинацией различных типов.

Существует два возможных способа определения макета группы. Например, когда макеты элементов организованы один за другим, он определяется как структура . С другой стороны, если макеты элементов выкладываются с одного и того же начального смещения, то это называется union .

Давайте создадим GroupLayout типа struct с целым числом и длинным :

GroupLayout groupLayout = MemoryLayout.ofStruct(MemoryLayouts.JAVA_INT, MemoryLayouts.JAVA_LONG);

Мы также можем создать groupplayout типа union , используя метод of Union :

GroupLayout groupLayout = MemoryLayout.ofUnion(MemoryLayouts.JAVA_INT, MemoryLayouts.JAVA_LONG);

Первый из них-это структура, которая содержит по одному из каждого типа. И, во-вторых, это структура, которая может содержать тот или иной тип.

Групповая компоновка позволяет нам создать сложную компоновку памяти, состоящую из нескольких элементов. Например:

MemoryLayout memoryLayout1 = MemoryLayout.ofValueBits(32, ByteOrder.nativeOrder());
MemoryLayout memoryLayout2 = MemoryLayout.ofStruct(MemoryLayouts.JAVA_LONG, MemoryLayouts.PAD_64);
MemoryLayout.ofStruct(memoryLayout1, memoryLayout2);

5. Нарезка сегмента памяти

Мы можем разрезать сегмент памяти на несколько более мелких блоков. Это позволяет избежать необходимости выделять несколько блоков, если мы хотим хранить значения с разными макетами.

Давайте попробуем использовать в качестве среза :

MemoryAddress memoryAddress = MemorySegment.allocateNative(12).baseAddress();
MemoryAddress memoryAddress1 = memoryAddress.segment().asSlice(0,4).baseAddress();
MemoryAddress memoryAddress2 = memoryAddress.segment().asSlice(4,4).baseAddress();
MemoryAddress memoryAddress3 = memoryAddress.segment().asSlice(8,4).baseAddress();

VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
intHandle.set(memoryAddress1, Integer.MIN_VALUE);
intHandle.set(memoryAddress2, 0);
intHandle.set(memoryAddress3, Integer.MAX_VALUE);

assertThat(intHandle.get(memoryAddress1), is(Integer.MIN_VALUE));
assertThat(intHandle.get(memoryAddress2), is(0));
assertThat(intHandle.get(memoryAddress3), is(Integer.MAX_VALUE));

6. Заключение

В этой статье мы узнали о новом API доступа к внешней памяти в Java 14.

Во-первых, мы рассмотрели необходимость доступа к внешней памяти и ограничения API до Java 14. Затем мы увидели, как API доступа к внешней памяти является безопасной абстракцией для доступа как к куче, так и к памяти без кучи.

Наконец, мы изучили использование API для чтения и записи данных как в куче, так и за ее пределами.

Как всегда, исходный код примеров доступен на GitHub .