предисловие
Понятно из буквального значения состоит в том, чтобы скопировать данные взад-вперед и назад не требуется, значительно улучшит производительность системы; слово мы часто слышим в Java Nio, Netty, Kafka, RocketMQ и других рамках, часто как основной момент его улучшения производительности ; ниже нескольких концепций от начала ввода / вывода, нулевой копии дальнейшего анализа.
Концепция ввода/вывода
1. Буфер
Буфер является основой всех операций ввода-вывода. Ввод-вывод — это не что иное, как перемещение данных в буфер или из него; когда процесс выполняет операцию ввода-вывода, он отправляет запрос операционной системе либо на очистку данных, в буфере (запись) или заполнение буфера (чтение); давайте посмотрим на общую блок-схему java-процесса, инициирующего запрос на чтение для загрузки данных:
Если процесс инициирует запрос на запись, ему также необходимо скопировать данные из пользовательского буфера в буфер сокета ядра, а затем скопировать данные на сетевую карту через DMA и отправить их наружу;
Вы можете подумать, что это пустая трата места, вам нужно каждый раз копировать данные из пространства ядра в пространство пользователя, поэтому появление нулевой копии должно решить эту проблему;
Что касается нулевого копирования, предусмотрено два метода: метод mmap+write и метод sendfile;
2. Виртуальная память
Все современные операционные системы используют виртуальную память, используя виртуальные адреса вместо физических.Преимущества этого:
1. Несколько виртуальных адресов могут указывать на один и тот же адрес физической памяти.
2. Объем виртуальной памяти может быть больше фактического доступного физического адреса;
Используя первую функцию, адрес пространства ядра и виртуальный адрес пространства пользователя могут быть сопоставлены с одним и тем же физическим адресом, чтобы DMA мог одновременно заполнять буфер, видимый для процессов ядра и пространства пользователя, как показано на следующем рисунке. фигура:
3. метод mmap+write
Используйте метод mmap+write, чтобы заменить исходный метод чтения+записи. реализуется виртуальный адрес в виртуальном адресном пространстве процесса.Отношение отображения один к одному; таким образом, исходный буфер чтения ядра может быть сохранен для копирования данных в пользовательский буфер, но буфер чтения ядра по-прежнему требуется чтобы скопировать данные в буфер сокета ядра, как показано на следующем рисунке:
4. метод отправки файла
Системный вызов sendfile был представлен в версии ядра 2.1 для упрощения процесса передачи данных между двумя каналами по сети. Введение системного вызова sendfile не только снижает репликацию данных, но и уменьшает количество переключений контекста, как показано на следующем рисунке:
Нулевая копия Java
1.MappedByteBuffer
FileChannel, предоставляемый java nio, предоставляет метод map(), который может установить сопоставление виртуальной памяти между открытым файлом и MappedByteBuffer. файл на диске; вызов метода get() позволит получить данные с диска, что отражает текущее содержимое файла, а вызов метода put() обновит файл на диске, а изменения, внесенные в файл, повлияют на другие Читатель тоже виден, давайте посмотрим на простой пример чтения, а затем проанализируем MappedByteBuffer:
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
File file = new File("D://db.txt");
long len = file.length();
byte[] ds = new byte[(int) len];
MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0,
len);
for (int offset = 0; offset < len; offset++) {
byte b = mappedByteBuffer.get();
ds[offset] = b;
}
Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
while (scan.hasNext()) {
System.out.print(scan.next() + " ");
}
}
}
Отображение в основном реализуется через функцию map(), предоставляемую FileChannel.Метод map() выглядит следующим образом:
public abstract MappedByteBuffer map(MapMode mode,
long position, long size)
throws IOException;
Предоставляются три параметра: MapMode, Position и size соответственно:
MapMode: режим отображения, варианты включают: READ_ONLY, READ_WRITE, PRIVATE;
Позиция: с какой позиции начать отображение, позиция количества байтов;
Размер: сколько байтов назад от позиции;
Давайте сосредоточимся на MapMode. Пожалуйста, два представляют только чтение и чтение-запись. Конечно, запрошенный режим карты ограничен правами доступа объекта Filechannel. Если READ_ONLY включен для файла без разрешения на чтение, будет выдано исключение NonReadableChannelException ; ЧАСТНЫЙ режим Представляет карту с копированием при записи, что означает, что любое изменение, выполненное с помощью метода put(), приводит к созданию частной копии данных, которая видна только экземпляру MappedByteBuffer; этот процесс не вносит никаких изменений в базовый file , и как только буфер будет очищен от мусора, эти изменения будут потеряны; взгляните на исходный код метода map():
public MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException
{
...省略...
int pagePosition = (int)(position % allocationGranularity);
long mapPosition = position - pagePosition;
long mapSize = size + pagePosition;
try {
// If no exception was thrown from map0, the address is valid
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
// An OutOfMemoryError may indicate that we've exhausted memory
// so force gc and re-attempt map
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
// After a second OOME, fail
throw new IOException("Map failed", y);
}
}
// On Windows, and potentially other platforms, we need an open
// file descriptor for some mapping operations.
FileDescriptor mfd;
try {
mfd = nd.duplicateForMapping(fd);
} catch (IOException ioe) {
unmap0(addr, mapSize);
throw ioe;
}
assert (IOStatus.checkAll(addr));
assert (addr % allocationGranularity == 0);
int isize = (int)size;
Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
if ((!writable) || (imode == MAP_RO)) {
return Util.newMappedByteBufferR(isize,
addr + pagePosition,
mfd,
um);
} else {
return Util.newMappedByteBuffer(isize,
addr + pagePosition,
mfd,
um);
}
}
Общий смысл в том, чтобы получить адрес карты памяти через нативный метод, если не получится, вручную gc map снова, наконец, экземпляр MappedByteBuffer создается через адрес карты памяти, сам MappedByteBuffer является абстрактным классом, по сути , реальный экземпляр здесь — DirectByteBuffer;
2.DirectByteBuffer
DirectByteBuffer наследуется от MappedByteBuffer.Из названия можно догадаться, что открыта прямая память, и она не занимает пространство памяти jvm; MappedByteBuffer, отображенный Filechannel в предыдущем разделе, на самом деле является DirectByteBuffer.Конечно, помимо этим методом вы также можете создать пространство вручную:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
Пространство прямой памяти объемом 100 байт открывается, как указано выше;
3. Межканальная передача
Часто бывает необходимо перенести файлы из одного места в другое. FileChannel предоставляет метод transferTo() для повышения эффективности передачи. Давайте сначала рассмотрим простой пример:
public class ChannelTransfer {
public static void main(String[] argv) throws Exception {
String files[]=new String[1];
files[0]="D://db.txt";
catFiles(Channels.newChannel(System.out), files);
}
private static void catFiles(WritableByteChannel target, String[] files)
throws Exception {
for (int i = 0; i < files.length; i++) {
FileInputStream fis = new FileInputStream(files[i]);
FileChannel channel = fis.getChannel();
channel.transferTo(0, channel.size(), target);
channel.close();
fis.close();
}
}
}
Данные файла передаются в канал System.out через метод TransferTo() FileChannel Интерфейс определяется следующим образом:
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
Некоторые параметры также легче понять, а именно позиция начала передачи, количество байтов для передачи и целевой канал TransferTo() позволяет перекрестно соединить один канал с другим, не требуя промежуточного буфера для передачи данных;
Примечание. Здесь есть два значения того, что промежуточный буфер не нужен: первому уровню не нужен буфер пользовательского пространства для копирования буфера ядра, а два других канала имеют свои собственные буферы ядра, и два буфера ядра также могут использоваться для копирования буфера ядра. нет необходимости копировать данные;
Нетти нулевая копия
Netty предоставляет буфер с нулевым копированием. При передаче данных окончательно обработанные данные должны быть объединены и разделены в одном переданном сообщении. Собственный ByteBuffer Nio не может этого сделать. Netty предоставляет Composite (комбинацию) и Slice через предоставленные (Split) два буфера для добиться нулевой копии, будет нагляднее увидеть следующую картину:
Сообщение HTTP на уровне TCP разделено на два ChannelBuffers, которые не имеют смысла для нашей логики верхнего уровня (обработка HTTP). Однако комбинация двух ChannelBuffers становится осмысленным HTTP-сообщением. ChannelBuffer, соответствующий этому сообщению, — это то, что можно назвать «сообщением», и здесь используется слово «Virtual Buffer».
Вы можете взглянуть на исходный код CompositeChannelBuffer, предоставленный netty:
public class CompositeChannelBuffer extends AbstractChannelBuffer {
private final ByteOrder order;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;
private final boolean gathering;
public byte getByte(int index) {
int componentId = componentId(index);
return components[componentId].getByte(index - indices[componentId]);
}
...省略...
Компоненты используются для сохранения всех полученных буферов, индексы записывают начальную позицию каждого буфера, lastAccessedComponentId записывает последний доступный ComponentId; CompositeChannelBuffer не открывает новую память и напрямую копирует все содержимое ChannelBuffer, а сохраняет его напрямую Все ссылки ChannelBuffer читаются и записываются в дочернем ChannelBuffer, достигнув нулевой копии.
другие
Сообщения RocketMQ последовательно записываются в файл журнала фиксации, а затем используют файл очереди потребления в качестве индекса; RocketMQ использует mmap+write с нулевым копированием для ответа на запросы потребителей;
Точно так же существует процесс сохранения большого количества сетевых данных на диск и отправки дисковых файлов по сети в kafka.Kafka использует метод нулевого копирования sendfile;
Суммировать
Если под нулевой копией просто понимать вероятность объекта в java, то фактически используется ссылка на объект, и каждая ссылка на объект может изменить объект, и всегда будет существовать только один объект.
Ссылаться на
<<java_nio>>