Знаете ли вы, что можно выделять сегменты памяти, которые больше, чем физический размер оперативной памяти вашего компьютера, и даже больше, чем размер всей вашей файловой системы? Прочтите эту статью и узнайте, как использовать сопоставленные (mapped) сегменты памяти, которые могут быть или не быть «разреженными», и как выделить 64 терабайта разреженных данных на ноутбуке.

Сопоставленная память

Сопоставленная память — это виртуальная память, которой было назначено взаимно-однозначное сопоставление с частью файла. Термин «файл» используется здесь в довольно широком смысле и может быть представлен обычным файлом, устройством, общей памятью или любым другим объектом, на который операционная система может ссылаться через дескриптор файла.

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

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

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

Настройка сопоставленного сегмента памяти

Новая функция Foreign Function and Memory, которая во второй раз появляется ​​в Java 20, позволяет сопоставлять большие сегменты памяти с файлом. Вот как можно создать сегмент памяти размером 4 ГБ, сопоставленный с файлом.

Set<OpenOption> opts = Set.of(CREATE, READ, WRITE);

try (FileChannel fc = FileChannel.open(Path.of("myFile"), opts);
     Arena arena = Arena.openConfined()) {

    MemorySegment mapped = 
            fc.map(READ_WRITE, 0, 1L << 32, arena.scope());

    use(mapped);
} // Ресурсы, выделенные с помощью "mapped" освобождаются здесь через TwR (try with resources)

Разреженные файлы

Разреженный файл — это файл, в котором информация может храниться эффективным образом, если фактически не все части файла фактически используются. Файл с большими неиспользуемыми «дырами» является примером такого файла, в котором только фактически используемые секции хранятся в основном физическом файле. В действительности, однако, неиспользуемые дыры также потребляют некоторое количество ресурсов, хотя и гораздо меньше, чем их используемые аналоги.

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

Пока разреженный файл не заполнен слишком большим количеством данных, можно выделить разреженный файл, размер которого намного больше доступного физического дискового пространства. Например, можно выделить пустой сегмент памяти объемом 10 ТБ, сопоставленный с разреженным файлом, в файловой системе с очень малой доступной емкостью.

Следует отметить, что не все платформы поддерживают разреженные файлы.

Настройка сегмента памяти с сопоставленного с разреженным файлом

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

Set<OpenOption> sparse = Set.of(CREATE_NEW, SPARSE, READ, WRITE);
try (var fc = FileChannel.open(Path.of("sparse"), sparse);
     var arena = Arena.openConfined()) {

     memorySegment mapped = 
             fc.map(READ_WRITE, 0, 1L << 32, arena.scope());

    use(mapped);
} // Ресурсы, выделенные с помощью "mapped" освобождаются здесь через TwR 

Примечание. Будет казаться, что файл состоит из 4 Гб данных, но на самом деле файл вообще не использует никакого (видимого) пространства файловой системы:

pminborg@pminborg-mac ntive % ll sparse 
-rw-r--r--  1 pminborg  staff  4294967296 Nov 14 16:12 sparse

pminborg@pminborg-mac ntive % du -h sparse 
  0B	sparse

Переход к гигантским файлам

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

 Я использую Mac M1 под управлением macOS Monteray (12.6.1) с 32 Гб оперативной памяти и 1 Тб дисковой памяти (из которых доступно 900 Гб).

Мне удалось отобразить один разреженный файл размером до 64 Тбайт, используя один сопоставленный сегмент памяти на моей машине (используя стандартные настройки):

  4 GiB -> ok as demonstrated above
  1 TiB -> ok
 32 TiB -> ok
 64 TiB -> ok
128 TiB -> failed with OutOfMemoryError

Можно увеличить объем сопоставленной памяти, но это выходит за рамки данной статьи. В реальных приложениях лучше отображать в память меньшие части разреженного файла, чем отображать весь разреженный файл в одном фрагменте. Эти меньшие сопоставления затем будут служить «окнами» в большой базовый файл.

 Во всяком случае, он выглядит довольно огромным:

-rw-r--r--   1 pminborg  staff  70368744177664 Nov 22 13:34 sparse

Создание пустого разреженного файла размером 64 Тб заняло на моей машине около 200 мс.

Замечание по ограничению потоков

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

Что дальше?

Попробуйте сопоставленные сегменты уже сегодня, загрузив предварительную сборку JDK: JDK 20 Early-Access Build. Не забудьте указать флаг --enable-preview JVM, иначе ваш код не запустится.

Комментарии (2)


  1. outlingo
    18.01.2023 23:46
    -3

    Java-программисты открывают для себя, что можно mmap'нуть спарзнутый файл.

    Все с ужасом и нетерпением ждут, что скоро Java-программисты узнают, что память в таких же объемах можно было просто malloc'нуть - и если не забивать ее cпециально нулями то достигается тот же самый эффект, что и с mmap спарзнутого файла.


    1. Beholder
      19.01.2023 17:57
      +1

      А Си-программисты до сих пор не открыли как buffer overflow избегать?