Java NIO channel

news2024/12/27 4:38:33

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 之间的关联信息。

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() 方法设置或修改兴趣集:

key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

3. 就绪集 (Ready Set)

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

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() 方法来设置和获取附加对象:

// 设置附加对象
key.attach(someObject);

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

5. 取消注册 (Cancelling the Registration)

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

key.cancel();

6. 通道和选择器的访问

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

  • channel():返回与 SelectionKey 关联的 SelectableChannel
  • selector():返回与 SelectionKey 关联的 Selector
SelectableChannel channel = key.channel();
Selector selector = key.selector();

7. 迭代选择的键

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

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

Selector selector = Selector.open();

2. 注册通道

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

// 创建一个 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 会更新其内部的状态,并返回准备好的通道数量。

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

4. 获取并处理就绪的键

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

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 并取消所有注册的通道。

selector.close();

6. 唤醒选择器

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

selector.wakeup();

7. 示例代码

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

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
    RandomAccessFile file = new RandomAccessFile("file.txt", "rw");
    FileChannel channel = file.getChannel();
    
  • 读取和写入

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

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

    • map():将文件的一部分或全部映射到内存中,返回一个 MappedByteBuffer
    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
    SocketChannel socketChannel = SocketChannel.open();
    
  • 连接服务器

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

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

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

    • 调用 close() 方法关闭 SocketChannel
    socketChannel.close();
    

3. ServerSocketChannel

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

  • 创建 ServerSocketChannel

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

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

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

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

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

4. DatagramChannel

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

  • 创建 DatagramChannel

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

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

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

    • 使用 send()receive() 方法发送和接收数据包。
    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 事件。
    SelectionKey key = datagramChannel.register(selector, SelectionKey.OP_READ);
    

5. AsynchronousFileChannel

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

  • 创建 AsynchronousFileChannel

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

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

    • 可以使用 CompletionHandler 来处理异步操作的结果。
    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
    Pipe pipe = Pipe.open();
    
  • 获取 SinkChannel 和 SourceChannel

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

    • 使用 write()read() 方法在 SinkChannelSourceChannel 之间传递数据。
    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() 等更先进的系统调用,它们可以在某些情况下避免用户空间与内核空间之间的数据复制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2254420.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构与算法学习笔记----堆

数据结构与算法学习笔记----堆 author: 明月清了个风 first publish time: 2024.12.2 revised: 2024.12.3 - 例题标题错误&#xff0c;已修改。 ps⛹从这里开始调整了文章结构&#xff0c;先讲解算法和数据结构基本原理&#xff0c;再给出例题&#xff0c;针对例题中的应用再…

【C++】格式化输出详解:掌握 cout 的进阶用法

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;格式化输出的理论概述&#x1f4af;控制输出宽度和填充字符setw 操作符setfill 操作符 &#x1f4af;控制浮点数的显示格式fixed 与 scientificsetprecision &#x1f4af;…

Rust : 生成日历管理markdown文件的小工具

需求&#xff1a; 拟生成以下markdown管理小工具&#xff0c;这也是我日常工作日程表。 可以输入任意时间段&#xff0c;运行后就可以生成以上的markdown文件。 一、toml [package] name "rust-workfile" version "0.1.0" edition "2021"[d…

Spring01——Spring Framework系统架构

spring学习路线 Spring Framework系统架构 相关概念 低耦合&#xff1a;是指系统中各个模块或组件之间的依赖性较低&#xff0c;即它们之间的联系较少、交互简单。这种设计原则的目的是为了提高系统的灵活性和可维护性&#xff0c;便于模块的独立开发、测试和替换。解耦&…

MicroBlaze软核开发(一):Hello World

实现功能&#xff1a;使用 MicroBlaze软核 串口打印 Hello World Vivado版本&#xff1a;2018.3 目录 MicroBlaze介绍 vivado部分&#xff1a; 一、新建工程 二、配置MicroBlaze 三、添加Uart串口IP 四、生成HDL文件编译 SDK部分&#xff1a; 一、导出硬件启动SDK 二、…

ZLMediaKit+wvp (ffmpeg+obs)推拉流测试

这里使用了两种方式: ffmpeg命令和 OBS OBS推流在网上找了些基本没有说明白的, 在ZLMediaKit的issues中看到了一个好大哥的提问在此记录一下 使用OBS推流&#xff0c;rtmp&#xff0c;报鉴权失败 推流 1. ffmpeg命令推流 官方说明文档地址: 推流规则 rtsp://192.168.1.4:10554…

微信小程序之简单的数据中心管理平台(1)

微信小程序之简单的数据中心管理平台&#xff08;1&#xff09; 引言 随着微信小程序的广泛应用&#xff0c;越来越多的企业开始探索如何利用这一技术开发高效、便捷的管理平台。数据中心管理作为信息化建设的重要组成部分&#xff0c;需要一个灵活、可扩展的界面来实现资源的…

【计算机网络】实验13:运输层端口

实验13 运输层端口 一、实验目的 本次实验旨在验证TCP和IP运输层端口号的作用&#xff0c;深入理解它们在网络通信中的重要性。通过实验&#xff0c;我将探讨端口号如何帮助区分不同的应用程序和服务&#xff0c;使得在同一台主机上能够同时运行多个网络服务而不发生冲突。此…

人工智能学习用的电脑安装cuda、torch、conda等软件,版本的选择以及多版本切换

接触人工智能的学习三个月了&#xff0c;每天与各种安装包作斗争&#xff0c;缺少依赖包、版本高了、版本低了、不兼容了、系统做一半从头再来了。。。这些都是常态。三个月把单位几台电脑折腾了不下几十次安装&#xff0c;是时候总结一下踩过的坑和积累的经验了。 以一个典型的…

数组常见查找算法

文章目录 时间复杂度1. 顺序查找&#xff08;Linear Search&#xff09;2. 二分查找&#xff08;Binary Search&#xff09;3. 插值查找&#xff08;Interpolation Search&#xff09;4.分块查找5.哈希查找 时间复杂度 衡量算法执行时间随输入规模增长而增长的速度的一个概念。…

langgraph 多智能体 Multi-agent supervisor

1. 工具定义 1.1网络搜索工具 from typing import Annotated import os from langchain_community.tools.tavily_search import TavilySearchResults from langchain_core.tools import tool from langchain_experimental.utilities import PythonREPLos.environ["TAVIL…

【Maven系列】深入解析 Maven 镜像配置

前言 Maven 是一个流行的 Java 项目管理和构建工具&#xff0c;可以自动化构建项目、管理依赖、生成报告等。在Maven构建项目时&#xff0c;通常经常需要下载各种依赖。默认情况下&#xff0c;Maven 会从中央仓库下载这些依赖&#xff0c;但在某些情况下&#xff0c;这个过程可…

HTML 添加 文本水印

body,html {margin: 0;height: 100vh;width: 100vw;} // 自定义文案const setting {text: "水印文案", // 水印内容innerDate: true, // 在水印下方增加日期width: 110, // 水印宽度};// 自定义文字水印const watermark (function () {return {build: function (a…

华为HCIE-Datacom认证笔试+实验考试介绍

华为HCIE数通认证考试是面向那些希望成为数通网络领域专家的人员&#xff0c;考试通常两部分&#xff1a;笔试和实验考试。 考试科目&#xff1a; HCIE-Datacom笔试考试内容&#xff1a; HCIE-Datacom V1.0考试覆盖数据通信领域路由交换高阶技术、企业网络架构全景、园区网络…

PyCharm+Selenium+Pytest配置小记

1、下载ChromeDriver&#xff1a; Chrome130以后的Driver下载&#xff1a; Chrome for Testing availabilityhttps://googlechromelabs.github.io/chrome-for-testing/ &#xff08;1&#xff09;查看自己Crome浏览器的版本&#xff1a;设置-->关于 Chrome&#xff1b; &…

我们来学webservie - WSDL

WSDL 题记WSDL系列文章 题记 举个例子 酒桌上大领导们谈笑风生&#xff0c;把酒临风,其喜洋洋者矣老张说能签下xx项目&#xff0c;一来证明了集团在行业中的翘楚地位&#xff0c;二来感谢各位领导给予的大力支持接下来的一周&#xff0c;项目经理、业务顾问相继入场&#xff0…

weblogic开启https

JSK证书生成 生成密钥库和证书 使用Java的keytool命令来生成一个Java密钥库&#xff08;Keystore&#xff09;和证书。keytool是Java开发工具包&#xff08;JDK&#xff09;中用于管理密钥库和证书的命令行工具。 #创建证书存放目录 [weblogicosb1 jksHL]$ mkdir -p /home/w…

激活函数在神经网络中的应用与选择

目录 ​编辑 Sigmoid函数 代码示例与分析 Tanh函数 代码示例与分析 ReLU函数 代码示例与分析 Leaky ReLU函数 代码示例与分析 PReLU函数 代码示例与分析 ELU函数 代码示例与分析 SELU函数 代码示例与分析 Softmax函数 代码示例与分析 结论 在深度学习领域&am…

使用Tauri创建桌面应用

当前是在 Windows 环境下 1.准备 系统依赖项 Microsoft C 构建工具WebView2 (Windows10 v1803 以上版本不用下载&#xff0c;已经默认安装了) 下载安装 Rust下载安装 Rust 需要重启终端或者系统 重新打开cmd&#xff0c;键入rustc --version&#xff0c;出现 rust 版本号&…

2023年第十四届蓝桥杯Scratch国赛真题—推箱子

推箱子 程序演示及其源码解析&#xff0c;可前往&#xff1a; https://www.hixinao.com/scratch/creation/show-188.html 若需在线编程&#xff0c;在线测评模考&#xff0c;助力赛事可自行前往题库中心&#xff0c;按需查找&#xff1a; https://www.hixinao.com/ 题库涵盖…