Java NIO channel

channel(通道),byteBuffer(缓冲区),selector(io多路复用),通道FileChannel,SocketChannel的transferTo,transferFrom,MappedByteBuffer实现了零拷贝。

JVM调操作系统方法,read,write,都可以送字节数组。

Java对从操作系统写入和读取的字符数组做了转化为基本数据类型。

channel

表示可以执行IO操作的对象

常用方法:

read(ByteBuffer dst):从通道读取数据到缓冲区。

write(ByteBuffer src):将缓冲区中的数据写入通道。

close():关闭通道。

isOpen():检查通道是否打开。

byteBuffer

缓冲区

ByteBuffer类,

allocate方法,分配堆内内存;

allocateDirect方法,分配堆外内存;

子类MappedByteBuffer,映射虚拟内存;

网络编程

IO多路复用

selector

SelectionKey 通道的状态和兴趣操作(accept,connect,read,write),SelectionKey包含了该通道与Selector` 之间的关联信息

SelectableChannel

在 Java NIO 中,SelectorSelectionKey 是实现非阻塞 I/O 的核心组件,它们共同工作以允许一个或多个线程管理多个通道(如 SocketChannelServerSocketChannel 等)。Selector 用于监控多个 SelectableChannel 对象的 I/O 操作(读、写、连接等),而 SelectionKey 则表示每个注册到 Selector 上的通道的状态和兴趣操作。下面详细解释 SelectionKey 的作用:

1. 注册通道与选择器

当一个 SelectableChannel 被注册到 Selector 上时,会返回一个 SelectionKey 对象。这个 SelectionKey 包含了该通道与 Selector 之间的关联信息。

java 复制代码
SelectableChannel channel = ...; // 例如 SocketChannel 或 ServerSocketChannel
Selector selector = Selector.open();
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

2. 兴趣集 (Interest Set)

SelectionKey 的兴趣集指定了我们希望 Selector 监控的 I/O 操作类型。常见的兴趣操作包括:

  • OP_ACCEPT :监听新连接的请求(仅适用于 ServerSocketChannel)。
  • OP_CONNECT :监听连接完成事件(适用于客户端 SocketChannel 的异步连接)。
  • OP_READ:监听可读事件。
  • OP_WRITE:监听可写事件。

你可以通过 interestOps() 方法设置或修改兴趣集:

java 复制代码
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

3. 就绪集 (Ready Set)

SelectionKey 的就绪集表示当前通道上哪些 I/O 操作已经准备好。每次 Selector 进行选择(调用 select()selectNow())后,它会更新所有注册的 SelectionKey 的就绪集。你可以通过 readyOps() 方法检查哪些操作已经准备好:

java 复制代码
int readySet = key.readyOps();
if ((readySet & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
    // 通道已准备好读取
}
if ((readySet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) {
    // 通道已准备好写入
}

4. 附加对象 (Attached Object)

SelectionKey 允许你将任意对象附加到键上,这通常用于存储与通道相关的上下文信息。你可以使用 attach()attachment() 方法来设置和获取附加对象:

java 复制代码
// 设置附加对象
key.attach(someObject);

// 获取附加对象
Object attachedObject = key.attachment();

5. 取消注册 (Cancelling the Registration)

如果你不再需要 Selector 监控某个通道,可以调用 cancel() 方法取消注册。这会使得 SelectionKey 在下次 select() 调用时被移除:

java 复制代码
key.cancel();

6. 通道和选择器的访问

SelectionKey 提供了方法来访问与其关联的通道和选择器:

  • channel() :返回与 SelectionKey 关联的 SelectableChannel
  • selector() :返回与 SelectionKey 关联的 Selector
java 复制代码
SelectableChannel channel = key.channel();
Selector selector = key.selector();

7. 迭代选择的键

当你调用 Selector.select()selectNow() 后,Selector 会返回一个包含所有就绪键的集合。你可以遍历这些键来处理就绪的 I/O 操作:

java 复制代码
Selector selector = Selector.open();
// 注册通道...

while (true) {
    // 等待通道准备就绪
    selector.select();

    // 获取所有就绪的 SelectionKey
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectedKeys.iterator();

    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        if (key.isAcceptable()) {
            // 处理接受连接
        } else if (key.isReadable()) {
            // 处理读取数据
        } else if (key.isWritable()) {
            // 处理写入数据
        }

        // 移除已处理的键
        iterator.remove();
    }
}

总结

SelectionKey 在 Java NIO 中扮演着至关重要的角色,它不仅记录了通道的兴趣操作和就绪状态,还提供了附加对象的功能,便于开发者在处理 I/O 事件时携带额外的上下文信息。通过 SelectionKey,你可以高效地管理和响应多个通道的 I/O 操作,从而实现高性能的网络应用程序。

Selector

Selector 是 Java NIO(New Input/Output)库中的一个重要组件,它允许单个线程管理多个 SelectableChannel(如 SocketChannelServerSocketChannel 等)。通过 Selector,你可以监控多个通道的 I/O 事件(如连接、读取、写入等),并在这些事件发生时得到通知。这使得编写高效的、非阻塞的网络应用程序成为可能,特别是在需要处理大量并发连接的情况下。

1. 创建 Selector

要使用 Selector,首先需要创建一个实例。可以通过调用 Selector.open() 方法来创建一个新的 Selector

java 复制代码
Selector selector = Selector.open();

2. 注册通道

要让 Selector 监控某个 SelectableChannel,你需要将该通道注册到 Selector 上,并指定你感兴趣的 I/O 操作(称为"兴趣集")。每个注册的通道会返回一个 SelectionKey,这个键包含了通道的状态和相关信息。

java 复制代码
// 创建一个 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 设置为非阻塞模式

// 绑定端口
serverChannel.bind(new InetSocketAddress(8080));

// 将通道注册到 Selector 上,并指定兴趣操作为 OP_ACCEPT
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);

3. 选择就绪的通道

Selector 提供了几个方法来选择已经准备好进行 I/O 操作的通道:

  • select():阻塞当前线程,直到至少有一个通道准备好了所注册的操作。
  • select(long timeout):阻塞当前线程,最多等待指定的时间(以毫秒为单位),如果在超时时间内没有任何通道准备好,则返回。
  • selectNow():立即返回,不阻塞,检查是否有任何通道准备好。

每次调用这些方法后,Selector 会更新其内部的状态,并返回准备好的通道数量。

java 复制代码
int readyChannels = selector.select(); // 阻塞直到有通道准备好
// 或者
int readyChannels = selector.select(1000); // 最多等待1秒
// 或者
int readyChannels = selector.selectNow(); // 立即返回

4. 获取并处理就绪的键

SelectorselectedKeys() 方法返回一个包含所有已准备好通道的 SelectionKey 集合。你可以遍历这个集合,检查每个 SelectionKey 的就绪状态,并根据需要处理相应的 I/O 操作。

java 复制代码
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();

while (iterator.hasNext()) {
    SelectionKey key = iterator.next();

    if (key.isAcceptable()) {
        // 处理新的连接请求
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(selector, SelectionKey.OP_READ);
    } else if (key.isReadable()) {
        // 处理读取数据
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = clientChannel.read(buffer);
        if (bytesRead == -1) {
            // 客户端关闭连接
            clientChannel.close();
        } else {
            // 处理接收到的数据
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear();
        }
    } else if (key.isWritable()) {
        // 处理写入数据
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
        clientChannel.write(buffer);
    }

    // 移除已处理的键
    iterator.remove();
}

5. 关闭 Selector

当你不再需要 Selector 时,应该调用 close() 方法来释放相关的资源。这将关闭 Selector 并取消所有注册的通道。

java 复制代码
selector.close();

6. 唤醒选择器

如果你在一个线程中调用了 select() 方法并且它正在阻塞,你可以通过调用 wakeup() 方法来唤醒它。这将使 select() 方法立即返回,即使没有通道准备好。

java 复制代码
selector.wakeup();

7. 示例代码

以下是一个完整的示例,展示了如何使用 Selector 来处理多个客户端连接:

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {

    public static void main(String[] args) throws IOException {
        // 创建 Selector
        Selector selector = Selector.open();

        // 创建 ServerSocketChannel 并绑定端口
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(8080));

        // 注册 ServerSocketChannel 到 Selector,监听连接请求
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server started on port 8080");

        while (true) {
            // 等待通道准备就绪
            selector.select();

            // 获取所有已准备好通道的 SelectionKey
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                if (key.isAcceptable()) {
                    // 处理新的连接请求
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = server.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("New client connected: " + clientChannel.getRemoteAddress());
                } else if (key.isReadable()) {
                    // 处理读取数据
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = clientChannel.read(buffer);
                    if (bytesRead == -1) {
                        // 客户端关闭连接
                        clientChannel.close();
                        System.out.println("Client disconnected: " + clientChannel.getRemoteAddress());
                    } else {
                        // 处理接收到的数据
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            System.out.print((char) buffer.get());
                        }
                        buffer.clear();
                    }
                }

                // 移除已处理的键
                iterator.remove();
            }
        }
    }
}

总结

Selector 是 Java NIO 中实现高效、非阻塞 I/O 的关键组件。它允许一个线程同时管理多个通道,并在这些通道上发生 I/O 事件时得到通知。通过结合 SelectionKeyByteBuffer,你可以轻松地构建高性能的网络服务器和客户端应用程序。

Java nio的channel相关的api

Java NIO(New Input/Output)库提供了多种 Channel 类,用于非阻塞 I/O 操作和更高效的文件、网络通信。与传统的 I/O 流不同,Channel 是双向的,既可以读取数据也可以写入数据,并且支持直接缓冲区(DirectByteBuffer)以提高性能。以下是 Java NIO 中常见的 Channel 类及其相关 API 的详细介绍:

1. FileChannel

FileChannel 用于对文件进行读写操作。它支持非阻塞模式,并且可以通过 transferTo()transferFrom() 方法实现零拷贝。

  • 创建 FileChannel

    • 通过 RandomAccessFileFileInputStreamFileOutputStream 获取 FileChannel
    java 复制代码
    RandomAccessFile file = new RandomAccessFile("file.txt", "rw");
    FileChannel channel = file.getChannel();
  • 读取和写入

    • 使用 read()write() 方法将数据从 FileChannel 读取到 ByteBuffer 或从 ByteBuffer 写入 FileChannel
    java 复制代码
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = channel.read(buffer); // 读取数据到缓冲区
    buffer.flip(); // 切换为读模式
    channel.write(buffer); // 将缓冲区中的数据写入通道
  • 零拷贝

    • transferTo():将文件内容直接传输到另一个 WritableByteChannel,如 SocketChannel
    • transferFrom():从 ReadableByteChannel 将数据传输到 FileChannel
    java 复制代码
    long position = 0;
    long count = channel.size();
    channel.transferTo(position, count, socketChannel); // 文件到套接字
  • 映射文件

    • map():将文件的一部分或全部映射到内存中,返回一个 MappedByteBuffer
    java 复制代码
    MappedByteBuffer mappedBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);

transferTo():

将文件内容从 FileChannel 直接传输到另一个 WritableByteChannel(如 SocketChannel),而不需要将数据加载到应用程序的内存中。

语法:long transferTo(long position, long count, WritableByteChannel target)

适用于文件到网络套接字的高效传输。

transferFrom():

从 ReadableByteChannel(如 SocketChannel)将数据传输到 FileChannel,同样不需要将数据完全加载到应用程序的内存中。

语法:long transferFrom(ReadableByteChannel src, long position, long count)

适用于从网络套接字接收数据并写入文件。

2. SocketChannel

SocketChannel 用于 TCP 网络通信,支持非阻塞模式和异步连接。

  • 创建 SocketChannel

    • 通过 SocketChannel.open() 创建一个新的 SocketChannel
    java 复制代码
    SocketChannel socketChannel = SocketChannel.open();
  • 连接服务器

    • 使用 connect() 方法连接到远程服务器。
    java 复制代码
    InetSocketAddress address = new InetSocketAddress("localhost", 8080);
    socketChannel.connect(address);
  • 非阻塞模式

    • 调用 configureBlocking(false) 设置为非阻塞模式。
    java 复制代码
    socketChannel.configureBlocking(false);
  • 读取和写入

    • 使用 read()write() 方法与远程服务器进行数据交换。
    java 复制代码
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = socketChannel.read(buffer); // 读取数据
    buffer.flip(); // 切换为读模式
    socketChannel.write(buffer); // 发送数据
  • 关闭连接

    • 调用 close() 方法关闭 SocketChannel
    java 复制代码
    socketChannel.close();

3. ServerSocketChannel

ServerSocketChannel 用于监听传入的 TCP 连接请求,通常与 Selector 一起使用来处理多个客户端连接。

  • 创建 ServerSocketChannel

    • 通过 ServerSocketChannel.open() 创建一个新的 ServerSocketChannel
    java 复制代码
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
  • 绑定端口

    • 使用 bind() 方法绑定到指定的本地地址和端口。
    java 复制代码
    InetSocketAddress address = new InetSocketAddress(8080);
    serverChannel.bind(address);
  • 接受连接

    • 使用 accept() 方法接受新的客户端连接,返回一个新的 SocketChannel
    java 复制代码
    SocketChannel clientChannel = serverChannel.accept();
  • 非阻塞模式

    • 调用 configureBlocking(false) 设置为非阻塞模式。
    java 复制代码
    serverChannel.configureBlocking(false);
  • 注册到 Selector

    • ServerSocketChannel 注册到 Selector 上,监听 OP_ACCEPT 事件。
    java 复制代码
    SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);

4. DatagramChannel

DatagramChannel 用于 UDP 网络通信,支持非阻塞模式和异步发送/接收数据包。

  • 创建 DatagramChannel

    • 通过 DatagramChannel.open() 创建一个新的 DatagramChannel
    java 复制代码
    DatagramChannel datagramChannel = DatagramChannel.open();
  • 绑定端口

    • 使用 bind() 方法绑定到指定的本地地址和端口。
    java 复制代码
    InetSocketAddress address = new InetSocketAddress(8080);
    datagramChannel.bind(address);
  • 非阻塞模式

    • 调用 configureBlocking(false) 设置为非阻塞模式。
    java 复制代码
    datagramChannel.configureBlocking(false);
  • 发送和接收数据

    • 使用 send()receive() 方法发送和接收数据包。
    java 复制代码
    ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
    InetSocketAddress remoteAddress = new InetSocketAddress("localhost", 8080);
    datagramChannel.send(buffer, remoteAddress); // 发送数据
    
    buffer.clear();
    InetSocketAddress senderAddress = (InetSocketAddress) datagramChannel.receive(buffer); // 接收数据
  • 注册到 Selector

    • DatagramChannel 注册到 Selector 上,监听 OP_READOP_WRITE 事件。
    java 复制代码
    SelectionKey key = datagramChannel.register(selector, SelectionKey.OP_READ);

5. AsynchronousFileChannel

AsynchronousFileChannel 提供了异步文件 I/O 的功能,可以在不阻塞主线程的情况下执行文件读写操作。

  • 创建 AsynchronousFileChannel

    • 通过 AsynchronousFileChannel.open() 创建一个新的 AsynchronousFileChannel
    java 复制代码
    Path path = Paths.get("file.txt");
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
  • 异步读取和写入

    • 使用 read()write() 方法异步读取和写入文件内容,返回一个 Future 对象。
    java 复制代码
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    Future<Integer> readResult = fileChannel.read(buffer, 0); // 异步读取
    Future<Integer> writeResult = fileChannel.write(buffer, 0); // 异步写入
  • 完成处理器

    • 可以使用 CompletionHandler 来处理异步操作的结果。
    java 复制代码
    fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            System.out.println("Read " + result + " bytes");
        }
    
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            System.err.println("Read failed: " + exc.getMessage());
        }
    });

6. Pipe

Pipe 是一种特殊的通道,用于在同一个 JVM 内的不同线程之间传递数据。它由两个部分组成:SinkChannel(写入端)和 SourceChannel(读取端)。

  • 创建 Pipe

    • 通过 Pipe.open() 创建一个新的 Pipe
    java 复制代码
    Pipe pipe = Pipe.open();
  • 获取 SinkChannel 和 SourceChannel

    • 使用 sink()source() 方法分别获取写入端和读取端。
    java 复制代码
    Pipe.SinkChannel sinkChannel = pipe.sink();
    Pipe.SourceChannel sourceChannel = pipe.source();
  • 读取和写入

    • 使用 write()read() 方法在 SinkChannelSourceChannel 之间传递数据。
    java 复制代码
    ByteBuffer buffer = ByteBuffer.wrap("Hello, Pipe!".getBytes());
    sinkChannel.write(buffer); // 写入数据
    buffer.clear();
    sourceChannel.read(buffer); // 读取数据

总结

Java NIO 提供了丰富的 Channel 类,涵盖了文件、TCP、UDP 以及异步 I/O 操作。这些类不仅支持非阻塞模式,还提供了高效的零拷贝和异步操作机制,使得开发者可以构建高性能的网络应用程序和文件处理系统。选择合适的 Channel 类取决于具体的应用场景和需求,例如是否需要处理大量并发连接、是否需要高效的数据传输等。

内存映射文件(MappedByteBuffer)在哪里分配缓冲区?和操作系统的page cache 的交互过程是啥?

内存映射文件(MappedByteBuffer)的缓冲区并不是像普通 ByteBuffer 那样直接在 JVM 的堆空间中分配,而是通过操作系统的虚拟内存管理机制来实现的。具体来说,MappedByteBuffer 的缓冲区是通过操作系统提供的内存映射功能分配的,它实际上映射到了操作系统的 页缓存(Page Cache) 中。下面我们将详细解释 MappedByteBuffer 的分配机制以及它与操作系统页缓存的交互过程。

1. MappedByteBuffer 的缓冲区分配

当调用 FileChannel.map() 方法时,JVM 会请求操作系统将文件的某个区域映射到进程的虚拟地址空间中。这个映射过程由操作系统的内存管理单元(MMU)处理,具体步骤如下:

  • 虚拟地址映射:操作系统会在进程的虚拟地址空间中为文件的指定区域分配一段虚拟地址。这段虚拟地址并不立即对应物理内存中的实际页面,而是一个虚拟映射。

  • 按需加载 :只有当程序访问这段虚拟地址中的数据时,操作系统才会根据需要将对应的文件页面从磁盘加载到物理内存中。这个过程称为 缺页中断(Page Fault)。操作系统会检查页表,找到对应的磁盘位置,然后将数据加载到物理内存中,并更新页表以记录新的映射关系。

  • 共享页缓存 :加载到物理内存中的页面会被存储在操作系统的 页缓存(Page Cache) 中。页缓存是操作系统用于缓存文件数据的内存区域,它可以加速对文件的读写操作。多个进程可以通过页缓存共享同一份文件数据,从而提高效率。

因此,MappedByteBuffer 的缓冲区并不是直接在 JVM 的堆空间中分配的,而是通过操作系统的虚拟内存管理机制映射到物理内存中的页缓存。这意味着 MappedByteBuffer 的内容实际上是存储在操作系统管理的物理内存中,而不是 JVM 的堆中。

2. MappedByteBuffer 与操作系统页缓存的交互过程

MappedByteBuffer 与操作系统页缓存之间的交互主要体现在以下几个方面:

2.1 读取操作
  • 按需加载 :当你通过 MappedByteBuffer 读取文件内容时,JVM 会通过虚拟地址访问数据。如果该虚拟地址对应的页面尚未加载到物理内存中,操作系统会触发 缺页中断,并将所需的页面从磁盘加载到页缓存中。

  • 缓存命中:如果该页面已经在页缓存中(即之前已经被加载过),操作系统可以直接从页缓存中返回数据,而不需要再次从磁盘读取。这大大提高了读取速度,因为访问物理内存的速度远快于访问磁盘。

  • 透明性 :对于应用程序来说,读取 MappedByteBuffer 的过程是透明的,你只需要像操作普通内存一样读取数据,而不需要显式地进行 I/O 操作。操作系统会自动处理页面的加载和缓存。

2.2 写入操作
  • 写时复制(Copy-On-Write) :如果你使用 READ_WRITEPRIVATE 模式对 MappedByteBuffer 进行写操作,操作系统会根据模式的不同处理写入行为:

    • READ_WRITE 模式 :写入的数据会直接反映到文件中。操作系统会将修改后的页面标记为脏页(Dirty Page),并在适当的时候将这些页面同步回磁盘。你可以通过调用 force() 方法强制将修改同步到磁盘。
    • PRIVATE 模式 :写入的数据不会影响原始文件。操作系统会使用 写时复制 机制,在你第一次写入某个页面时,操作系统会创建该页面的副本,并将修改应用到副本上,而原始文件保持不变。
  • 脏页管理 :当 MappedByteBuffer 中的数据被修改后,操作系统会将这些页面标记为脏页。脏页会保留在页缓存中,直到操作系统决定将其写回到磁盘。通常,操作系统会在系统空闲时或内存压力较大时将脏页同步回磁盘。

  • 同步到磁盘 :你可以通过调用 MappedByteBuffer.force() 方法显式地将脏页同步到磁盘。这确保了文件的修改被持久化,避免在系统崩溃时丢失数据。

2.3 换出和换入
  • 换出(Swap Out) :当物理内存不足时,操作系统可能会将不常用的页面换出到磁盘上的交换文件(Swap File)。这包括 MappedByteBuffer 映射的页面。换出的过程是透明的,应用程序不会感知到这一变化。当程序再次访问这些页面时,操作系统会重新将它们从交换文件加载回物理内存。

  • 换入(Swap In) :当程序访问一个已经被换出的页面时,操作系统会触发 缺页中断,并将该页面从交换文件加载回物理内存。这个过程与从磁盘加载文件页面类似,但性能稍差,因为交换文件通常是位于较慢的存储设备上。

2.4 内存回收
  • 自动回收 :当 MappedByteBuffer 对应的 FileChannel 被关闭时,操作系统会自动回收该映射的虚拟地址空间。然而,操作系统并不会立即释放物理内存中的页面,除非这些页面不再被任何进程使用。这意味着即使 MappedByteBuffer 已经被垃圾回收,操作系统仍然可能保留这些页面在页缓存中,以备后续使用。

  • 手动清理 :在某些情况下,你可能希望显式地清理 MappedByteBuffer 映射的内存。Java 8 及之后版本提供了一个 sun.misc.Cleaner 类,可以通过反射调用其 clean() 方法来强制清理 MappedByteBuffer。然而,这种方法是非标准的,且不推荐在生产环境中使用。

3. MappedByteBuffer 与 JVM 堆空间的关系

MappedByteBuffer 的缓冲区并不在 JVM 的堆空间中分配,因此它的大小不受 JVM 堆大小的限制。相反,MappedByteBuffer 的大小取决于操作系统的虚拟内存管理和可用的物理内存。这意味着你可以映射非常大的文件,而不会导致 JVM 堆溢出(OutOfMemoryError)。

然而,虽然 MappedByteBuffer 不占用 JVM 堆空间,但它仍然会占用操作系统的虚拟地址空间。在 32 位系统上,虚拟地址空间是有限的(通常为 4GB),因此映射过大的文件可能会导致虚拟地址空间耗尽。在 64 位系统上,虚拟地址空间非常大,这个问题较少发生。

4. 优点与注意事项

优点:
  • 高性能 :通过利用操作系统的页缓存和按需加载机制,MappedByteBuffer 可以显著提高文件 I/O 的性能,尤其是在处理大文件时。
  • 简化编程模型:你可以像操作普通内存一样访问文件内容,代码更加简洁。
  • 异步加载:操作系统会在需要时自动将文件的数据从磁盘加载到内存,而不是一次性加载整个文件,节省了内存并提高了响应速度。
  • 支持随机访问:你可以直接访问文件的任意位置,而不需要从头开始读取。
注意事项:
  • 内存使用 :虽然 MappedByteBuffer 不占用 JVM 堆空间,但它会占用操作系统的虚拟地址空间。在 32 位系统上,虚拟地址空间是有限的,因此需要注意不要映射过大的文件。
  • 持久性问题 :在某些情况下,操作系统可能不会立即将修改后的数据写回到磁盘,这可能导致数据丢失。你可以使用 force() 方法强制将修改同步到磁盘。
  • 安全性问题 :由于 MappedByteBuffer 允许直接访问文件内容,因此在多线程环境中使用时需要注意线程安全问题,避免多个线程同时修改同一部分数据。
  • 内存回收MappedByteBuffer 对应的内存映射区域不会自动释放,即使 MappedByteBuffer 对象被垃圾回收。为了确保资源正确释放,建议在不再需要时显式地关闭相关的 FileChannel

5. 总结

MappedByteBuffer 是 Java NIO 中一个强大的工具,它通过内存映射文件技术提供了高效的文件 I/O 操作。MappedByteBuffer 的缓冲区并不是直接在 JVM 的堆空间中分配的,而是通过操作系统的虚拟内存管理机制映射到物理内存中的页缓存。操作系统负责管理页面的加载、换入换出和脏页同步,确保文件数据的高效访问和持久化。理解 MappedByteBuffer 与操作系统页缓存的交互过程,可以帮助开发者更好地优化文件 I/O 性能,并避免潜在的问题。

linux mmap()+write()` 的组合是一种实现零拷贝(或称为"半零拷贝")的方法

mmap() + write() 的组合是一种实现零拷贝(或称为"半零拷贝")的方法,它通过将文件内容映射到进程的地址空间,然后使用 write() 系统调用来发送这些数据。这种方式减少了从内核空间到用户空间的数据复制次数,但仍然需要一次从用户空间到套接字缓冲区的复制。下面是这个过程的一个简化图示:

+---------------------+         +---------------------+
|                     |         |                     |
|   用户空间 (User)   |         |  内核空间 (Kernel)  |
|                     |         |                     |
|  +--------------+   |         |  +--------------+   |
|  | mmap() 映射  |   |         |  | 文件缓存     |   |
|  | 区域         |<--+---------+->| (Page Cache) |   |
|  +--------------+   |         |  +--------------+   |
|                     |         |                     |
|  +--------------+   |         |  +--------------+   |
|  | write()      |   |         |  | 套接字缓冲区 |   |
|  | 发送数据     |---+-------->+->| (Socket Buf) |   |
|  +--------------+   |         |  +--------------+   |
|                     |         |                     |
+---------------------+         +---------------------+

具体步骤说明

  1. 文件映射 (mmap()):

    • 应用程序调用 mmap() 系统调用,将文件的一部分或全部映射到进程的虚拟地址空间。
    • 这个操作不会立即将文件内容加载到内存中,而是创建了一个虚拟内存区域,当应用程序访问这个区域时,操作系统会按需将文件内容加载到物理内存(页缓存)中。
  2. 数据传输 (write()):

    • 应用程序可以直接访问 mmap() 创建的内存区域,读取文件内容。
    • 然后,应用程序调用 write() 系统调用,将这些数据写入一个套接字或其他文件描述符。
    • 在这一步,数据会被从用户空间复制到内核空间的套接字缓冲区中,准备发送给网络接口卡(NIC)或其他目的地。

图解中的箭头表示

  • 虚线箭头 表示 mmap() 操作创建了用户空间和内核空间之间的映射关系,但并不立即发生数据复制。
  • 实线箭头 表示实际的数据复制操作,即当 write() 被调用时,数据从用户空间被复制到内核空间的套接字缓冲区。

注意事项

  • mmap()write() 组合虽然减少了部分数据复制,但并不是完全的零拷贝,因为 write() 仍然需要将数据从用户空间复制到内核空间。
  • 对于大文件或大量数据的传输,这种方法可以显著减少CPU的负担和上下文切换,提高性能。
  • 使用 mmap() 时需要注意文件大小和系统资源的限制,以及正确处理内存映射区域的同步问题。

如果你想要实现真正的零拷贝,可以考虑使用 sendfile() 或者 splice() 等更先进的系统调用,它们可以在某些情况下避免用户空间与内核空间之间的数据复制。

相关推荐
优雅的落幕7 分钟前
多线程---线程安全(synchronized)
java·开发语言·jvm
A charmer7 分钟前
Linux 权限管理:用户分类、权限解读与常见问题剖析
linux·运维·服务器
Charlie__ZS8 分钟前
帝可得-设备管理
java·maven·intellij-idea·idea
运维自动化&云计算11 分钟前
华为交换机与锐捷交换机端口链路聚合的配置
服务器·网络·华为
爱上语文13 分钟前
请求响应:常见参数接收及封装(数组集合参数及日期参数)
java·开发语言·spring boot·后端
孙同学_16 分钟前
【Linux篇】权限管理 - 用户与组权限详解
java·linux·服务器
CQU_JIAKE25 分钟前
926[study]Docker,DHCP
java·开发语言
浪 子28 分钟前
SpringBoot mq快速上手
java·spring boot·spring
程序猿进阶30 分钟前
Tomcat 都有哪些核心组件
java·开发语言·后端·面试·性能优化·tomcat·firefox
猫猫的小茶馆32 分钟前
【Linux系统】Linux内核框架(详细版本)
linux·运维·服务器·开发语言·嵌入式软件