Новое API для работы с нативной памятью

Denis Gabaydulin
3 min readAug 18, 2020

--

Одно из лучших свойств JVM — это постоянная эволюция. Год за годом инженеры из Oracle и других компаний пытаются делать JVM/JDK лучше. Цели амбициозны. Не последнюю роль играет и производительность. Но JVM это не только Java, но и десятки других, самых разных языков. JVM все ближе к тому, чтобы стать универсальной платформой для разработки, которая станет решать все больше задач. А Java является флагманским языком.

Моя область интересов это высокопроизводительные распределенные системы, базы данных и прочие штуки. Также, мне интересны разного рода оптимизации. Как макро, так и микро. Об одной из таких оптимизаций, и как это стало возможно, я собираюсь написать в этой заметки.

Предыстория. Несколько лет назад, Unsafe был объявлен вне закона. Не стану повторять все аргументы того времени, но есть мнение, что большие объемы памяти нужно выделять и освобождать вручную, потому что какой-то общий автоматический способ не знает ничего про конкретное приложение, и не может делать это оптимально. А на таких объемах это играет важную роль.

Каково реальное применение? Это могут быть большие кеши (64 GB и выше). Как это сделать с помощью Unsafe? Для этого есть метод allocateMemory(), который принимает на вход размер в байтах. Когда работа делается на таком низком уровне нужно быть очень внимательным и аккуратным. Выделенную память необходимо инициализировать (например нулями), а когда она более не нужна — освободить. Кроме того, были (а может есть и сейчас или еще будут) разные баги, связанные с данным методом. Короче говоря, это реально опасная штука.

На смену отдельным методам в Unsafe приходит публичное API, которое во-первых, является полноценным API с обычном поддержкой, а во-вторых, решает часть недостатков, которые были в Unsafe. Об одной такой замене, мы поговорим.

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

В JDK 14 и старше, в инкубаторе появилось новое API — Foreign-Memory Access. Что в нем интересного? Оно безопасно настолько, насколько это возможно (проверка адреса, перед обращением; выделенная память проинициализирована нулями). Посмотрим на код вставки.

public void put(long key, long value) {
Preconditions.checkArgument(key != NO_KEY, "Key " + NO_KEY + " is not supported!");
Preconditions.checkArgument(key != DELETED_KEY, "Key " + DELETED_KEY + " is not supported!");
int start = (maxSize - 1) & (noHash ? noHash() : hashLong(key)); int slot = start;
long keyElement = NO_KEY;
while (slot < maxSize) {
keyElement = (long) keyHandle.get(base, slot);
if (keyElement == NO_KEY || keyElement == DELETED_KEY || keyElement == key) {
break;
}
slot++;
}
boolean notFound = keyElement != NO_KEY && keyElement != DELETED_KEY && keyElement != key; if (notFound) {
// lets try to find slot at the beginning
int max = slot;
slot = 0;
keyElement = NO_KEY;
while (slot < max) {
keyElement = (long) keyHandle.get(base, slot);
if (keyElement == NO_KEY || keyElement == DELETED_KEY || keyElement == key) {
break;
}
slot++;
}
}
notFound = keyElement != NO_KEY && keyElement != DELETED_KEY && keyElement != key; if (notFound) {
throw new IllegalStateException("Not enough space!");
}
keyHandle.set(base, slot, key);
valueHandle.set(base, slot, value);
}

base — то начальный адрес в памяти, для сегмента, который мы выделяли при инициализации.
keyHandle и valueHandle — это var handles, через которые осуществляется доступ.
slot — это своего рода индекс в “массиве”. Так как ключ — это long, я заранее задал layout, который говорит как именно будет устроен режим адресации.

Например, если я пишу:

keyHandle.set(base, slot, key);

Это значит, что я записываю в ячейку по адресу base + (key_size * slot) значение ключа key. Поддерживается также и режим, в котором можно оперировать оффсетами в байтах.

Убедимся, что при вставке или чтении не выделяется лишней памяти.

Как видно, у нас нет ни аллокаций, ни работы GC! Полный код можно найти на github.

Задачи написать код совсем без аллокаций как правило нет, потому что программа большая, и как минимум, всю ее бенчмарками не покроешь. Да и читаемость кода упадет. Но некоторые отдельные компоненты программы, такие как кеши, или какой-то очень горячий кусок вашего common path, вполне можно оптимизировать. Теперь, мы можем делать это легально и без смс.

--

--