Узнайте о файлах Java, отображенных в память, и научитесь читать и записывать содержимое из файла, отображенного в память, с помощью RandomAccessFile и MemoryMappedBuffer.
1. Java-память-отображаемый ввод-вывод
Если вы знаете , как работает java IO на более низком уровне, то вы будете знать об обработке буферов, подкачке памяти и других подобных концепциях. Для обычного файлового ввода-вывода, в котором пользовательские процессы выдают системные вызовы read() и write() для передачи данных, почти всегда есть одна или несколько операций копирования для перемещения данных между этими страницами файловой системы в пространстве ядра и областью памяти в пространстве пользователя. Это происходит потому, что обычно нет выравнивания один к одному между страницами файловой системы и пользовательскими буферами.
Однако существует специальный тип операции ввода-вывода, поддерживаемый большинством операционных систем, который позволяет пользовательским процессам максимально использовать преимущества странично-ориентированной природы системного ввода-вывода и полностью избегать копирования буфера. Это называется вводом-выводом с отображением в память, и мы собираемся узнать здесь несколько вещей о файлах с отображением в память.
2. Файлы Java, отображенные в память
Отображенный в памяти ввод-вывод использует файловую систему для установления отображения виртуальной памяти из пользовательского пространства непосредственно на соответствующие страницы файловой системы. С отображенным в памяти файлом мы можем притвориться, что весь файл находится в памяти и что мы можем получить к нему доступ, просто рассматривая его как очень большой массив. Такой подход значительно упрощает код, который мы пишем для изменения файла.
Подробнее: Работа с буферами
Чтобы выполнять как запись, так и чтение в отображенных в память файлах, мы начинаем с RandomAccessFile, получаем канал для этого файла. Отображенные в память буферы байтов создаются с помощью метода FileChannel.map(). Этот класс расширяет класс ByteBuffer операциями, которые специфичны для отображенных в память областей файлов.
Отображенный буфер байтов и отображение файла, которое он представляет, остаются действительными до тех пор, пока сам буфер не будет очищен сборщиком мусора. Обратите внимание, что вы должны указать начальную точку и длину региона, который вы хотите отобразить в файле; это означает, что у вас есть возможность отображать меньшие регионы большого файла.
Пример 1: Запись в файл, отображенный в памяти
import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;public class MemoryMappedFileExample{static int length = 0x8FFFFFF;public static void main(String[] args) throws Exception{try(RandomAccessFile file = new RandomAccessFile("howtodoinjava.dat", "rw")){MappedByteBuffer out = file.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);for(int i = 0; i < length; i++){out.put((byte) 'x');}System.out.println("Finished writing");}}}
Файл, созданный с помощью указанной выше программы, имеет длину 128 МБ, что, вероятно, больше, чем позволяет ваша ОС. Файл кажется доступным сразу, потому что только его части переносятся в память, а другие части выгружаются. Таким образом, очень большой файл(до 2 ГБ) можно легко изменить.
3. Режимы сопоставления файлов
Как и обычные файловые дескрипторы, файловые сопоставления могут быть доступны для записи или только для чтения.
- Первые два режима отображения, MapMode.READ_ONLY и MapMode.READ_WRITE, довольно очевидны. Они указывают, хотите ли вы, чтобы отображение было доступно только для чтения или чтобы разрешалось изменение отображенного файла.
- Третий режим, MapMode.PRIVATE, указывает, что вы хотите отображение копирования при записи. Это означает, что любые изменения, которые вы вносите через put(), приведут к созданию частной копии данных, которую может видеть только экземпляр MappedByteBuffer.
В базовый файл не будут внесены никакие изменения, а все внесенные изменения будут утеряны при сборке мусора буфера. Несмотря на то, что сопоставление копирования при записи предотвращает любые изменения в базовом файле, вы должны открыть файл для чтения/записи, чтобы настроить сопоставление MapMode.PRIVATE. Это необходимо для того, чтобы возвращаемый объект MappedByteBuffer разрешал put().
Вы заметите, что нет метода unmap(). После установки сопоставление остается в силе до тех пор, пока объект MappedByteBuffer не будет удален сборщиком мусора.
Также сопоставленные буферы не привязаны к каналу, который их создал. Закрытие связанного FileChannel не уничтожает сопоставление; только удаление самого буферного объекта нарушает сопоставление.
MemoryMappedBuffer имеет фиксированный размер, но файл, к которому он привязан, является эластичным. В частности, если размер файла изменяется во время действия отображения, часть или весь буфер может стать недоступным, могут быть возвращены неопределенные данные или могут быть выброшены непроверенные исключения.
Будьте осторожны с тем, как файлы обрабатываются другими потоками или внешними процессами, когда они отображаются в памяти.
4. Преимущества файлов, отображенных в памяти
Отображение ввода-вывода в памяти имеет ряд преимуществ по сравнению с обычным вводом-выводом:
- Пользовательский процесс рассматривает данные файла как память, поэтому нет необходимости выполнять системные вызовы read() или write().
- Когда пользовательский процесс касается отображенного пространства памяти, автоматически генерируются ошибки страниц для загрузки данных файла с диска. Если пользователь изменяет отображенное пространство памяти, затронутая страница автоматически помечается как грязная и впоследствии будет сброшена на диск для обновления файла.
- Подсистема виртуальной памяти операционной системы будет выполнять интеллектуальное кэширование страниц, автоматически управляя памятью в соответствии с нагрузкой на систему.
- Данные всегда выравниваются по странице, и копирование буфера не требуется.
- Очень большие файлы можно отображать без использования большого объема памяти для копирования данных.
5. Как прочитать файл, отображенный в памяти
Чтобы прочитать файл с помощью отображенного в память ввода-вывода, используйте следующий шаблон кода:
import java.io.File;import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;public class MemoryMappedFileReadExample{private static String bigExcelFile = "bigFile.xls";public static void main(String[] args) throws Exception{try(RandomAccessFile file = new RandomAccessFile(new File(bigExcelFile), "r")){//Get file channel in read-only modeFileChannel fileChannel = file.getChannel();//Get direct byte buffer access using channel.map() operationMappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());// the buffer now reads the file as if it were loaded in memory.System.out.println(buffer.isLoaded()); //prints falseSystem.out.println(buffer.capacity()); //Get the size based on content size of file//You can read the file from this buffer the way you like.for(int i = 0; i < buffer.limit(); i++){System.out.print((char) buffer.get()); //Print the content of file}}}}
6. Как записать в файл, отображенный в памяти
Чтобы записать данные в файл с помощью отображенного в память ввода-вывода, используйте следующий шаблон кода:
import java.io.File;import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;public class MemoryMappedFileWriteExample {private static String bigTextFile = "test.txt";public static void main(String[] args) throws Exception{// Create file objectFile file = new File(bigTextFile);//Delete the file; we will create a new filefile.delete();try(RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")){// Get file channel in read-write modeFileChannel fileChannel = randomAccessFile.getChannel();// Get direct byte buffer access using channel.map() operationMappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 * 8);//Write the content using put methodsbuffer.put("howtodoinjava.com".getBytes());}}}
Оставляйте свои комментарии и мысли в разделе комментариев.