NIO比常规IO快在哪里。

明确一个关键点:系统调用(用户态到内核态的切换)是无法避免的。真正的性能提升来自于减少了不必要的数据拷贝。
常规IO:两次拷贝与状态切换
如流程图左侧路径所示,当我们使用常规IO(如FileInputStream读取一个文件)时:
-
发起系统调用(状态切换):您的Java线程发起
read系统调用,从用户态切换到内核态。 -
第一次拷贝(内核态):内核将数据从磁盘文件读取到系统缓冲区(Page Cache)。
-
第二次拷贝(状态切换 + 拷贝):内核需要将数据从系统缓冲区拷贝到您Java代码中提供的
byte[]数组(位于JVM堆内存)。这个拷贝过程本身是由内核态代码完成的,但因为它需要写入用户态空间,所以伴随着上下文切换。完成后,线程切换回用户态。 -
应用程序访问:您的Java代码才能访问
byte[]中的数据。
这个过程涉及两次数据拷贝和至少两次上下文切换。
直接内存:一次拷贝与内存映射
如流程图右侧路径所示,直接内存的妙处在于它开辟了一条“捷径”。当使用NIO的FileChannel和ByteBuffer.allocateDirect()时:
-
分配直接内存:
allocateDirect()会在JVM堆外分配一块内存区域。这块直接内存虽然属于Java进程的地址空间,但内核可以直接访问它。 -
建立映射(系统调用):
FileChannel.map()方法会发起系统调用(如mmap),请求内核将文件数据直接映射到这块直接内存区域。这同样会发生用户态到内核态的切换。 -
数据加载:当应用程序需要访问数据时,内核通过缺页中断将文件数据从磁盘加载到系统缓冲区。此时,由于映射关系已经建立,应用程序可以直接通过指针访问直接内存中的对应数据,而无需内核再进行一次显式的拷贝。
这个过程中,数据从系统缓冲区到Java进程空间的第二次拷贝被消除了。虽然系统调用和状态切换仍然存在,但最耗时的额外数据拷贝步骤被优化掉了 。
核心区别与性能影响
|
特性 |
常规IO(JVM堆缓冲区) |
直接内存(NIO) |
|---|---|---|
|
内存位置 |
JVM堆内,受GC管理 |
JVM堆外,由操作系统管理 |
|
数据流转 |
磁盘 → 系统缓冲区 → JVM堆缓冲区 |
磁盘 → 系统缓冲区 →(直接映射)→ 直接内存 |
|
拷贝次数 |
两次 |
一次(或零次,依赖内存映射) |
|
状态切换 |
至少两次(调用和返回) |
至少两次(调用和返回) |
|
性能 |
相对较低,受GC压力影响 |
更高,尤其适合大文件或高频IO |
|
管理复杂度 |
低,自动GC回收 |
高,需注意内存分配与释放(通常通过 |
重要提醒:权限与“直接”的含义
您提到的“不转换用户态”是一个常见的误解。更准确的描述是:直接内存的分配和映射的建立,本身就需要通过系统调用(即状态切换)来完成 。它的“直接”体现在映射建立之后,内核在后台处理IO时,无需再将数据显式地拷贝到一块独立的用户空间缓冲区。您的Java应用程序和内核共享了这块内存区域的访问视图,从而避免了冗余拷贝。
希望这个解释能彻底解开您的疑惑。直接内存是Java实现高性能IO的基石,理解其背后的机制对深入掌握网络编程、文件处理等场景至关重要。

浙公网安备 33010602011771号