предисловие
IO можно просто разделить на磁盘 IO
и网络 IO
,磁盘 IO
относительно网络 IO
Скорость будет быстрее, эта статья в основном знакомит磁盘 IO
,网络 IO
Напишите на следующей неделе.
Пара JAVANIO
абстрактно какChannel
, Channel
можно еще разделить наFileChannel
(Диск ио) иSocketChannel
(сетевой ио).
Если ваше понимание IO остается только на уровне API, этого недостаточно.Вы должны понимать, как IO обрабатывается на системном уровне, чтобы не попасть в ненужные ямы.
Содержание этой статьи:
- Использование FileChannel для чтения и записи для копирования файлов.
- Введение в ByteBuffer
- блокировка файлового процесса jvm, FileLock
- Кто быстрее, HeapbyteBuffer, DirectbyteBuffer или MMAP?
- от
Linux 内核
середина虚拟内存
,系统调用
,文件描述符
,Inode
,Page Cache
,缺页异常
Опишите весь процесс ввода-вывода - Как восстановить память DirectByteBuffer вне кучи jvm
Схемы, относящиеся к компьютерной системе в этой статье, взяты из книги «Углубленное понимание компьютерных систем».
Понимание Linux приходит из книг и справочных материалов.Содержание этой статьи в основном является моим собственным пониманием и проверкой кода.Некоторые описания могут быть неточными, но процесса понимания достаточно.
NIO
NIO
Представленный в Java 1.4, он называется неблокирующим вводом-выводом, а также называется новым вводом-выводом.
NIO реферирует какChannel
Он ориентирован на буфер (работает с фрагментом данных), неблокирующий ввод-вывод.
Channel
Отвечает только за передачу, данные отправляютсяBuffer
отвечает за хранение.
Buffer
Buffer
серединаcapacity
,limit
иposition
Атрибуты важнее, если вы их не понимаете, вы столкнетесь со многими ямами при чтении и записи файлов.
capacity
логотипBuffer
Максимальная емкость данных, равная длине массива.
limit
это указатель, определяющий максимальный индекс данных, которыми можно манипулировать в текущем массиве.
position
Представляется как индекс при чтении следующих данных
@Test
public void run1() {
// `DirectByteBuffer`
final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
// `HeapByteBuffer`
final ByteBuffer allocate = ByteBuffer.allocate(1024);
}
HeapByteBuffer
будет выделено вJvm堆内
, ограниченный размером кучи JVM, скорость создания высокая, но скорость чтения и записи низкая. Фактический нижний слой представляет собой массив байтов.
DirectByteBuffer
назначитJvm 堆外
, не ограниченный размером кучи JVM, медленный для создания, быстрый для чтения и записи.DirectByteBuffer
В Linux память принадлежит куче процесса.DirectByteBuffer
В зависимости от параметров JVMMaxDirectMemorySize
Влияние.
Установите кучу jvm на 100 м и запустите программу, чтобы сообщить об ошибкеException in thread "main" java.lang.OutOfMemoryError: Java heap space
.因为指定了 jvm 堆为 100m,然后一些 class 文件也会放在 堆中的,实际堆内存时不足 100m,当申请 100m 堆内存只能报错了。
public class BufferNio {
// -Xmx100m
public static void main(String[] args) throws InterruptedException {
// HeapByteBuffer 是 jvm 堆内,因为堆不足分配 100m(java 中的一些 class 也会占用堆),导致 oom
System.out.println("申请 100 m `HeapByteBuffer`");
Thread.sleep(5000);
ByteBuffer.allocate(100 * 1024 * 1024);
}
}
Установите кучу jvm на 100 м, MaxDirectMemorySize на 1 г и создайте бесконечный циклDirectByteBuffer
, напечатать 10 раз申请 directbuffer 成功
, сообщить об ошибкеException in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
, я расскажу об этом вне кучи позжеDirectByteBuffer
Как утилизировать.
public class BufferNio {
// -Xmx100m -XX:MaxDirectMemorySize=1g
public static void main(String[] args) throws InterruptedException {
System.out.println("申请 100 m DirectByteBuffer");
final ArrayList<Object> objects = new ArrayList<>();
while (true) {
// DirectByteBuffer 不在 jvm 堆内,所以可以申请成功,但是不是无限制的,也有限制(MaxDirectMemorySize)
final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);
objects.add(byteBuffer);
System.out.println("申请 directbuffer 成功");
System.out.println(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage());
System.out.println(ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage());
}
}
}
FileChannel
прочитать файл
@Test
public void read() throws IOException {
final Path path = Paths.get(FILE_NAME);
// 创建一个 FileChannel,指定这个 channel 读写的权限
final FileChannel open = FileChannel.open(path, StandardOpenOption.READ);
// 创建一个和这个文件大小一样的 buffer,小文件可以这样,大文件,循环读
final ByteBuffer allocate = ByteBuffer.allocate((int) open.size());
open.read(allocate);
open.close();
// 切换为读模式,position=0
allocate.flip();
// 用 UTF-8 解码
final CharBuffer decode = StandardCharsets.UTF_8.decode(allocate);
System.out.println(decode.toString());
}
записать файл
@Test
public void write() throws IOException {
final Path path = Paths.get("demo" + FILE_NAME);
// 通道具有写权限,create 标识文件不存在的时候创建
final FileChannel open = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
final ByteBuffer allocate = ByteBuffer.allocate(1024);
allocate.put("张攀钦aaaaa-1111111".getBytes(StandardCharsets.UTF_8));
// 切换写模式,position=0
allocate.flip();
open.write(allocate);
open.close();
}
копировать файл
@Test
public void copy() throws IOException {
final Path srcPath = Paths.get(FILE_NAME);
final Path destPath = Paths.get("demo" + FILE_NAME);
final FileChannel srcChannel = FileChannel.open(srcPath, StandardOpenOption.READ);
final FileChannel destChannel = FileChannel.open(destPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// transferTo 实现类中,用的是一个 8M MappedByteBuffer 做数据的 copy ,但是这个方法只能 copy 文件最大字节数为 Integer.MAX
srcChannel.transferTo(0, srcChannel.size(), destChannel);
destChannel.close();
srcChannel.close();
}
FileLock
FileLcok
Это блокировка файла процесса jvm, которая действует между несколькими процессами jvm.Процесс имеет права на чтение и запись в файл, а также разделяемые блокировки и эксклюзивные блокировки.
Один и тот же процесс не может заблокировать повторяющуюся область одного и того же файла, и его можно заблокировать без дублирования.
В этом же процессе первый поток блокирует (0, 2) файла, а другой поток одновременно блокирует (1, 2).Если область блокировки файла повторяется, программа сообщит об ошибке .
Один процесс блокируется (0, 2), а другой процесс блокируется (1, 2), это нормально, потому чтоFileLock
является блокировкой процесса JVM.
Запустите следующую программу дважды, чтобы распечатать результаты
Первая программа печатается гладко
获取到锁0-3,代码没有被阻塞
获取到锁4-7,代码没有被阻塞
Вторая программа печатает
获取到锁4-7,代码没有被阻塞
获取到锁0-3,代码没有被阻塞
Когда запускается первая программа,file_lock.txt
Позиция 0-2 заблокирована.Первая программа удерживает блокировку 10 с.При запуске второй программы она заблокируется и будет ждать здесь.FileLock
, пока первая программа не снимет блокировку.
public class FileLock {
public static void main(String[] args) throws IOException, InterruptedException {
final Path path = Paths.get("file_lock.txt");
final FileChannel open = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.READ);
final CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
try (final java.nio.channels.FileLock lock = open.lock(0, 3, false)) {
System.out.println("获取到锁0-3,代码没有被阻塞");
Thread.sleep(10000);
final ByteBuffer wrap = ByteBuffer.wrap("aaa".getBytes());
open.position(0);
open.write(wrap);
Thread.sleep(10000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
try (final java.nio.channels.FileLock lock = open.lock(4, 3, false)) {
System.out.println("获取到锁4-7,代码没有被阻塞");
final ByteBuffer wrap = ByteBuffer.wrap("bbb".getBytes());
open.position(4);
open.write(wrap);
} catch (IOException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
open.close();
}
}
При изменении второй поток вышеуказанной программы вjava.nio.channels.FileLock lock = open.lock(1, 3, false)
Поскольку регион не позволяет повторение одного и того же файла блокировки процесса, программа будет жаловаться.
Exception in thread "Thread-1" java.nio.channels.OverlappingFileLockException
У кого выше эффективность чтения и записи между HeapByteBuffer и DirectByteBuffer?
FileChannel
класс реализацииFileChannelImpl
, при чтении и письмеByteBuffer
будет определять, будет лиDirectBuffer
, если нет, создастDirectBuffer
, скопируйте исходные данные буфера вDirectBuffer
Использовать.所以读写效率上来说,DirectByteBuffer 读写更快。 ноDirectByteBuffer
Создание относительных затрат времени.
несмотря на то чтоDirectByteBuffer
вне кучи, но когда использование памяти вне кучи достигает-XX:MaxDirectMemorySize
Когда нет возможности восстановить память за пределами кучи, также будет запущен FullGC, и будет выброшен OOM.
// 下面这个程序会一直执行下去,但是会触发 FullGC,来回收掉堆外的直接内存
public class BufferNio {
// -Xmx100m -XX:MaxDirectMemorySize=1g
public static void main(String[] args) throws InterruptedException {
System.out.println("申请 100 m `HeapByteBuffer`");
while (true) {
// 当前对象没有被引用,GC root 也就到达不了 DirectByteBuffer
ByteBuffer.allocateDirect(100 * 1024 * 1024);
System.out.println("申请 directbuffer 成功");
}
}
}
Создано бесконечным цикломDirectByteBuffer
Если не придет GC ROOT, объект будет переработан. При переработке он будет переработан только из кучи. Как выполняется переработка вне кучи?
отDirectByteBuffer
Источник продолжить, вы можете увидеть, что он имеет переменную участникаprivate final Cleaner cleaner;
, когда запускается FullGC, потому чтоcleaner
корень gc недоступен, что приводит кcleaner
Он будет переработан, и он будет активирован, когда он будет переработан.Cleaner.clean
(Инициируется вызовом метода Reference.tryHandlePending), преобразовательDirectByteBuffer.Deallocator
например, этот метод запуска, называемыйUnsafe.freeMemory
Чтобы освободить память вне кучи.
public class Cleaner extends PhantomReference<Object> {
private final Runnable thunk;
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
}
карта памяти
Когда приложение читает файл, данные должны быть прочитаны с диска в пространство ядра (сначала чтение и запись, без данных кэша страниц), а затем скопированы из пространства ядра в пространство пользователя, чтобы приложение можно использовать прочитанные данные data. Когда все данные файла находятся в кэше страниц ядра, нет необходимости читать их с диска, и они напрямую копируются из пространства ядра в пространство пользователя.
Когда приложение записывает данные в файл, оно сначала копирует записываемые данные в кэш страниц ядра, а затем вызываетfsync
Диск данных из ядра в файл (пока вызов возвращается успешно, данные не будут потеряны). или не звониfsync
Когда данные помещаются на диск, пока данные приложения записываются в кэш страниц ядра, операция записи завершается, и данные помещаются на диск путем内核
Планировщик Io поместит диск в нужное время (внезапный сбой питания приведет к потере данных, а такие программы, как MySQL, сами сохранят размещение данных).
Мы можем видеть данные данных, пройдут через копию из пространства пользователя и пространства ядра. Если вы можете удалить эту копию, эффективность будет намного выше, это MMAP (карта памяти). Указывая на память о пространстве пользовательского пространства и пространства ядра к тому же части физической памяти.内存映射
английский этоMemory Mapping
,сокращениеmmap
. Соответствующий системный вызовmmap
Таким образом, данные считываются и записываются в пользовательском пространстве, а фактическая операция также выполняется в пространстве ядра, что уменьшает количество копий данных.
Как этого добиться, проще говоря, адрес процесса в linux — это виртуальный адрес, и процессор будет отображать виртуальный адрес на физический адрес физической памяти. mmap фактически сопоставляет виртуальный адрес пользовательского процесса и виртуальный адрес пространства ядра с одной и той же физической памятью, что уменьшило количество копий данных.
Пользовательская программа вызывает системный вызовmmap
Последующее чтение и запись данных не требует вызова системного вызоваread
иwrite
.
Отображение виртуальной памяти на физическую память
Оперативную память компьютера можно представить как массив из M последовательных байтов, каждый байт имеет уникальный物理地址
(Physical Address
).
Используемый ЦП虚拟寻址
(VA
,Virtual Address
) Найти физический адрес.
CPU
будет использоваться процессом虚拟地址
Аппаратно на ЦП内存管理单元
(Memory Management Unit
MMU
) для выполнения преобразования адресов, чтобы найти физический адрес в физической основной памяти для получения данных.
Когда процесс загружается, система назначает虚拟地址空间
, когда определенное место в виртуальном адресном пространстве虚拟地址
При использовании он будет сначала сопоставлен с основной памятью.物理地址
.
Когда нескольким процессам необходимо совместно использовать данные, им нужно только сопоставить некоторые виртуальные адреса в их виртуальном адресном пространстве с одним и тем же физическим адресом.
Обычно, когда мы оперируем данными, мы не оперируем байт за байтом, что слишком неэффективно Обычно к некоторым байтам обращаются постоянно. Таким образом, при управлении памятью пространство памяти делится на управляемые страницы, и物理页
(Physical Page
), в виртуальной памятиVirtual Page
справляться. Типичный размер страницы составляет 4 КБ.
систему через MMU и页表(Page Table)
справляться虚拟页
и物理也
Соответствующим отношением таблицы страниц является запись таблицы страниц (Page Table Entry,PTE
) массив
Когда действительность PTE равна 1, идентификационные данные находятся в памяти, а когда идентификация равна 0, идентификация находится на диске.
Когда данные, соответствующие доступному виртуальному адресу, больше не находятся в физической памяти, возможны два случая:
1. Когда памяти достаточно, данные на диске, соответствующие виртуальной странице, будут напрямую загружены в физическую память.
2. При нехватке памяти срабатывает своп, согласно LRU виртуальная страница, соответствующая последней использованной виртуальной странице, также будет исключена, записана на диск, а часть данных в физической памяти будет удалена. Соответствующая виртуальная страница устанавливается в 0, а затем данные с диска загружаются в память.
обрабатывать виртуальную память
Linux
Каждому процессу назначается отдельный адрес виртуальной памяти,
Когда наша программа запускается, вместо одновременной загрузки всех файлов кода всей программы в память выполняется отложенная загрузка.
机械硬盘使用扇区来管理磁盘,磁盘控制器会通过块管理磁盘,系统通过 Page Cache 与磁盘控制器打交道。
一个块包含多个扇区,一个页也包含多个块。
磁盘上会有一个文件对应一个 Inode,Innode 记录文件的元数据及数据所在位置。
当系统启动的时候,这些 Inode 数据会被加载到主存中去。不过系统中的 Inode 还记录他们对应的物理内存中的位置(实际就是对应 Page Cache),有的 Inode 对应的数据没有加载到内存中,Inode 就不会记录其对应的内存地址。
程序执行之前会初始化其虚拟内存,虚拟内存会记录代码对应哪些 Innode。
Когда программа выполняется, система инициализирует виртуальную память текущей программы, а затем запускаетmain
функция, когда код выполнения найден, какой-то код не загружен в память, это вызовет исключение ошибки страницы, и будет найдена соответствующая виртуальная страница.Innoe
, затем загрузить нужные данные с диска в память, затем пометить виртуальную страницу как загруженную в память, и следующий доступ будет прямо из памяти.
##mmap в Java
Глядя на исходный код, который мы нашлиopen.map
также вернулсяDirectByteBuffer
, просто метод возвращаетDirectByteBuffer
использует другой конструктор, который связываетfd
. Когда мы читаем и записываем данные, системные вызовы чтения и записи не будут запускаться, что является преимуществом отображения памяти.
public class MMapDemo {
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
final URL resource = MMapDemo.class.getClassLoader().getResource("demo.txt");
final Path path = Paths.get(resource.toURI());
final FileChannel open = FileChannel.open(path, StandardOpenOption.READ);
// 发起系统调用 mmap
final MappedByteBuffer map = open.map(FileChannel.MapMode.READ_ONLY, 0, open.size());
// 读取数据时,不会再出发调用 read,直接从自己的虚拟内存中即可拿数据
final CharBuffer decode = StandardCharsets.UTF_8.decode(map);
System.out.println(decode.toString());
open.close();
Thread.sleep(100000);
}
}
Хотя это тожеDirectByteBuffer
, но отличается от mmap тем, что не привязан к fd.При чтении и записи данных ему все равно придется проходить копирование из пространства пользователя в пространство ядра, а также будут происходить системные вызовы, что относительно неэффективно по сравнению с mmap .
public class MMapDemo {
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
final URL resource = MMapDemo.class.getClassLoader().getResource("demo.txt");
final Path path = Paths.get(resource.toURI());
final FileChannel open = FileChannel.open(path, StandardOpenOption.READ);
// 这个 DirectByteBuffer 使用的构造不一样,它会走系统调用 read
final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
final int read = open.read(byteBuffer);
byteBuffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(byteBuffer).toString());
Thread.sleep(100000);
}
}
Системные вызовы для отслеживания кода, используемого в Linuxstrace
#!/bin/bash
rm -fr /nio/out.*
cd /nio/target/classes
strace -ff -o /nio/out java com.fly.blog.nio.MMapDemo
Скорость чтения и записи данныхmmap
больше, чемByteBuffer.allocateDirect
больше, чемByteBuffer.allocate
.
Эта статья написанаБлог Чжан Паньциня www.mflyyou.cn/творчество. Ее можно свободно воспроизводить и цитировать, но с обязательной подписью автора и указанием источника статьи.
При перепечатке в публичную учетную запись WeChat добавьте QR-код публичной учетной записи автора в конец статьи. Имя общедоступной учетной записи WeChat: Mflyyou