Содержание

понедельник, 24 октября 2011 г.

Java off-heap cache

Не так давно на хабре я описывал как бороться с паузами java приложения, не трогая GC. Один из вариантов, вокруг которого в последнее время поднимается большой ажиотаж, это выделение памяти вне Heap. Давайте же посмотрим какие инструменты имеет java разработчик для реализации данного подхода, и что из готового уже существует на рынке.
ByteBuffer
Начиная с java 1.4, в которой вышел в свет NIO, в арсенале разработчиков появился очень интересный класс java.nio.ByteBuffer. У него есть метод allocateDirect(), который согласно javadoc может выделить массив памяти вне Heap.
ByteBuffer buf = ByteBuffer.allocateDirect(1024*1024*1024);
buf.putShort(0, (short)0x1234);
buf.putInt(2, 0x12345678);
buf.putLong(8, 0x1122334455667788L);
Если посмотреть исходники, то можно обнаружить, что он использует нативный метод класса sun.misc.Unsafe, который и занимается выделением памяти за пределами Heap. Здесь важно понимать, что выделение такой памяти более затратно, чем создание массива непосредственно в куче. Что же касается записи и чтения из данного массива, то по некоторым утверждениям она быстрее чем в Heap. Однако с освобождением места, выделенного через ByteBuffer, могут возникнуть проблемы, так как оно удаляется через механизм финализации, вернее, если говорить более строго, то через механизм фантомных ссылок. Т.е. выделенное место будет освобождено только тогда, когда в куче останется мало свободного места и будет вызван GC. А так как в данном случае мы работает с памятью, выделенной вне Heap, то получается маленькая логическая неувязка. Именно отсюда произрастают рекомендации, не создавать много ByteBuffer объектов для выделения памяти вне кучи и использовать его только для больших объемов памяти, а так же активно их кэшировать. Однако на данный момент существует небольшой workaroud для данной проблемы. Это выставление свойства JVM -XX:MaxDirectMemorySize=, которое будет инициировать Full GC, когда размер памяти, выделенной в direct buffer, будет достигать выставленного ограничения. Еще одно ограничение ByteBuffer состоит в том, что нет возможности выделить памяти больше 2G (размер, передаваемый в качестве параметра, ограничен int), так что если требуется выделить больше памяти, нужно будет выделять её кусками и писать логику чанкинга.

Без сомнения самым известным продуктом, если не сказать единственным, является BigMemory от Terracora. К великому огорчению данный продукт является платным. Но, судя по форумам, и тому, что они просят выставлять параметр -XX:MaxDirectMemorySize, реализовано их хранилище на основе ByteBuffer.

Но в мире open-source тоже не все так грустно. Буквально на днях в инкубаторе Apache появился новый proposal DirectMemory, который начал разрабатываться еще в начале года одним из блоггеров. Идея тут следующая. На запуске выделить большой массив байтов в прямом буфере и следить там за свободным местом, используя free-lists. Но перейдя под ASF, возможно, её заметно перепишут. Я списывался с автором, и он сказал, что есть надежда выйти  с твердым GA релизом в ближайшие несколько месяцев.

Есть еще одна наработка в этом направлении от Vanilla Java. Над ним тоже работает несколько человек в домашних условиях. Но он заточен под хранение больших коллекций, а не просто для кэширования ключ-значение.


RandomAccessFile on RamDisk

Еще один способ обойти Heap java - это создать обычный файл, но не на жестком диске,а на диске замапленном на оперативную память. В линукс можно создать файл в разделе /dev/shm. В качестве примера можно привести свободную версию EHCache с хранилищем на диске замапленном на память. Но, по заверению компании Terracota, данное решение будет работать медленнее, и главным его недостатком, по сравнению с BigMemory, является то, что ключи в данном случае будут храниться в Heap, так что кэша с бешеным количеством маленьких объектов с ним не сваришь.
sun.misc.Unsafe
Unsafe unsafe = initUnsafe();
long address = unsafe.allocateMemory(2L);
unsafe.putShort(address, (short)25);
System.out.println(unsafe.getShort(address));
unsafe.freeMemory(address);
Преимущество этого подхода перед использованием ByteBuffer - это возможность освобождать память самостоятельно, без косвенного участия GC. Так же размер выделяемой памяти не ограничен int. Зато работать с ним придется вручную, так как нет удобных оберток для записи\чтения примитивов, а так же самому надо следить за такими вещами как, например, byte order на разных платформах. Ну и конечно же, используя закрытое API, вам никто не гарантирует совместимости с будущими версиями. Между тем данный подход успешно применили Одноклассники для кэширования данных профиля пользователя и чего-то там еще, о чем кратко описали в обзоре своей архитектуры на хабре.
Memory Mapped File
File file = new File("/tmp/example.dat");
FileChannel channel = new RandomAccessFile(file, "rw").getChannel();
MappedByteBuffer buf = channel.map(MapMode.READ_WRITE, 0L, 16L*1024*1024*1024);
С помощью того же стандартного пакета nio есть возможность получить MappedByteBuffer, который расширяет ByteBuffer. Но здесь есть опасность нежданного IO. Зато легко реализовать сбрасывание snapshot памяти на диск и его загрузку на стартапе. Т.е. мы получаем эдакую дополнительную память, которую можно во время рестарта легко и быстро сохранить на диск и потом его зачитать. Чтобы избежать IO можно замапить файл с того же RamDisk или /dev/shm. Бонус в отличие от RandomAccessFile будет в том, что для работы с памятью у вас будет удобный и функциональный интерфейс ByteBuffer. А еще вроде бы эта память должна быть доступна сразу нескольким процессам, и есть возможность выделить любое количество памяти, не ограниченное int.

Примеров здесь, к сожалению, привести не могу.
JNI
С помощью JNI и какого-нибудь Си можно добраться до shared memory. Например, используя такие библиотеки как, Apache's Tomcat Native Libraries или JIPC.

В Cassandra с версии 1.0 использует данный подход по умолчанию.
 
В заключении
Забавно, все описанные механизмы доступны с java 1.4, т.е. совсем скоро уже десятилетие, а активно развиваться начали только совсем недавно. Возможно раньше большие объемы Heap встречались довольно редко, в силу дороговизны оперативной памяти. Так что ждем хорошего open-source решения в ближайшее время! Было бы оно интересно вам? Примите участие в опросе на хабре.
Ссылки
Microbenchmark для описанных подходов
Как работает ByteBuffer в деталях
DirectMemory
Terracota BigMemory
Hazelcast Elastic Memory