NIO 的底层机理
线程的内存划分
在进程的眼里自己是拥有所有的内存空间的,这就是虚拟内存技术的强大之处,那么在它们眼里,这一块空间是怎么进行划分的呢?下面这一段复杂的图,只需要了解一下,图中最重要的就是【栈】和【堆】

栈 & 堆
栈和堆使用了不同的数据结构,方法,空间等等,他们之间存在着许多的区别,但是两者就像是形状不同的拼图,只有两个拼图都在才能组成完整的画面,只有堆和栈在一起,才能更好地管理进程内存空间
- 栈是一个先进后出,后进先出的数据结构,当程序使用函数调用,声明变量等其他类型的操作后栈会生成对应的栈帧,栈帧这一数据结构中会存储与函数相关的信息,包括函数中的局部变量,调用的其他函数的命令等等
- 堆中存储的数据结构较多,存储复杂数据结构,比如对象或是自定义数据结构等等,堆内的空间是可变的,随着用户的使用堆中空间会进行部分的扩容,以满足更多复杂数据结构的需求
栈中的空间固定,且较小,而堆中的空间大,但无序,所以一旦使用引用数据类型时会使用栈内指针指向堆内空间。既使用了栈快捷便捷的数据结构来加快 CPU 的执行效率,又使用能自主变化大小的堆来存储复杂多样的数据结构
注:无论是栈还是堆,他们都是在进程内存上的一部分,CPU 给进程分配的用户空间是有限的,不加以注意可能会导致栈溢出或者堆溢出
补充:
线程方面
同一进程被分配的空间中,
- 栈对于线程来说是私有的,每个线程独有一个栈;
- 堆对于线程来说是共有的,一个进程中的所有线程都共用一个堆;
回收方面
栈与堆的内存回收方式不同
栈中一旦出栈的数据都会被销毁,栈空间是由操作系统自动分配和管理的,创建和销毁栈的操作都不需要程序员手动干预。当程序执行完一个函数后,操作系统就会自动回收该函数的局部变量和临时变量所占用的栈空间。
堆中的空间由程序员自己进行分配,堆中的空间需要手动处理释放,而在 C/C++ 和 Java/Python/GO 中的堆回收机制不同
- C/C++:需要程序员手动回收不需要的堆内空间
- Java/Python/GO:引入 GC 机制,根据算法判定堆内哪一个对象是需要被释放的
- Java - 可达性分析算法: JVM 中采取了【可达性分析算法】,当堆中对象不由栈内强指针直接指向时,该对象会被回收释放
以上是一部分关于栈与堆之间的信息,更加详细的信息可见:
堆、栈,它们到底是什么?看完这篇文章你就明白了!_堆和栈-CSDN博客
mmap 在 Java 底层是怎么实现的
关于 mmap 如何使用
Java 在 JDK 1.4 之后引入了 NIO
BIO 是面向流传输的,而 NIO 是面向块传输的
在Java中,内存映射文件通过FileChannel和MappedByteBuffer类实现。以下是实现内存映射文件的基本步骤:
- 打开文件通道:使用
FileChannel.open()方法打开文件。 - 创建映射缓冲区:使用
FileChannel.map()方法创建映射缓冲区。 - 读写数据:使用映射缓冲区进行读写操作。
- 取消映射:使用
MappedByteBuffer.force()方法将缓冲区中的数据写入文件,并使用MappedByteBuffer.cleaner().clean()方法取消映射。
接下来来看一下 mmap 应该怎么被使用,从 FileChannel 连接,再到 MapperByteBuffer 映射最后再传输
NIO 的 mmap 使用流程是:开通道 -> 建映射 -> 像操作内存数组一样读写 -> 刷盘 -> 释放(GC)
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class MmapDemo {
public static void main(String[] args) {
// 文件路径
String filePath = "mmap_test.dat";
// 预设映射大小 (例如 100 MB)
long size = 100 * 1024 * 1024;
// 使用 try-with-resources 自动关闭 FileChannel
// 注意:这只会关闭 Channel,不会自动 Unmap (取消映射)
try (FileChannel fileChannel = FileChannel.open(Paths.get(filePath),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
System.out.println("1. 通道已打开,准备映射...");
// ============================================================
// 核心步骤:创建 MappedByteBuffer
// MapMode.READ_WRITE: 读写模式
// 0: 起始位置
// size: 映射大小 (这会直接在磁盘上占用 100MB 空间)
// ============================================================
MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);
System.out.println("2. 映射成功!获得直接内存引用。");
// ============================================================
// 写入操作 (像操作数组一样操作文件)
// ============================================================
// 写入一些字节
mappedBuffer.put("Hello NIO Mmap".getBytes());
// 跳转到指定位置写 (体现随机读写优势)
mappedBuffer.position(1024);
mappedBuffer.putInt(99999);
System.out.println("3. 数据已写入内存映射区 (Page Cache)。");
// ============================================================
// 性能关键:强制刷盘
// 操作系统通常会异步刷盘,force() 确保数据立即落盘 (类似 fsync)
// ============================================================
mappedBuffer.force();
System.out.println("4. 数据已强制刷入磁盘。");
// ============================================================
// 读取操作
// ============================================================
// 重置指针到开头
mappedBuffer.position(0);
byte[] buffer = new byte[14];
mappedBuffer.get(buffer);
System.out.println("5. 读取到的字符串: " + new String(buffer));
// 读取刚才在 1024 位置写的整数
mappedBuffer.position(1024);
System.out.println("6. 读取到的整数: " + mappedBuffer.getInt());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Mmap 增加了内存映射部分和改为使用块传递数据(或者说这个块被映射到内核空间中)当块满了之后会通知 DMA 拷贝数据...
Java mmap 底层是怎么实现的?
1、Java 进程当可能需要访问某个文件时,进程会发起请求,CPU 会开辟一段内核空间,并与进程中的用户空间进行映射
2、当 Java 进程真正请求这个文件时,CPU 会发现缺页,OS 会对 CPU 产生缺页中断,随后通知 DMA 将 Java 进程需要的数据传递给内核空间中对应的映射区域
3、当文件映射完成后,Java 进程就能够“看到”文件已经存在自己的空间中了,实则是 Java 进程的用户进程空间逻辑地址对应内核空间的物理地址
mmap 技术打通了用户空间与内核空间的壁垒,让我们能像操作内存数组一样高效地操作文件。







