java中使用mmap技术的实现示例
作者:Java编程爱好者
前言
jdk21 之后,随着 FFM 加入并稳定,现在 java 中也可以直接使用 mmap 技术将文件直接映射进内存并读取了,并且没有 nio 中 21 亿的限制(Integer.MAX_VALUE)。
BIO时代
try (FileInputStream fis = new FileInputStream("file.txt")) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理 buffer 中的前 bytesRead 字节
}
}
最早读取文件就是阻塞读取,由于 byte[] 是jdk管理,自己传入的,所以下标最大就是21亿,而且由于正常情况下由代码传入,所以不可能 new byte[Integer.MAX_VALUE],而且 byte[] 回收需要看 gc。
NIO时代(JDK 1.4+)
try (FileChannel channel = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ)) {
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
}
后来 java 推出了 NIO,但是作者本人实际工作接触的需要如此优化的场景太少,且 api 并不直观,导致完全没有用过几次。返回的 MappedByteBuffer api 中下标参数使用的是 int,这直接导致超过 4G 的文件必须分段映射,且 MappedByteBuffer 映射的内存也需要 gc 回收。
属于初代 mmap 技术,虽然原理上是 mmap,但是限制颇多。
AIO时代(JDK 7+)
本质上是 NIO.2,或者叫 new NIO,原则上属于NIO的一部分
AsynchronousFileChannel
这个 api 更是查无此人,作者并不知道这个应该怎么用/有什么性能。
FFM(JDK 22+)
try (var arena = Arena.ofConfined();
var channel = FileChannel.open(path, StandardOpenOption.READ)) {
var size = channel.size();
var memorySegment = channel.map(FileChannel.MapMode.READ_ONLY,
0,
size,
arena
);
} catch (IOException e) {
throw new RuntimeException(e);
}
随着 FFM 稳定,原本的 FileChannel api 中也加入了对 FFM 的支持,使用 Arena 配置接下来申请的内存的生命周期管理,再将 arena 传入 channel,返回 MemorySegment,使用 MemorySegment 即可对全文件进行随机读写。arena 关闭后申请的所有内存直接回收,没有 gc 压力。
由于调用的是底层的 mmap 返回的内存段,所以直接读写 memorySegment 即可直接反应到文件上,也可以调用 MemorySegment#force() 强制写入。
更加接近底层的 mmap 技术,可选择是否有 gc 压力。
读取示例:
public class MemorySegmentFileTest {
private static final Path path = Path.of("/home/bin-/Downloads/home/extraData.img");
static void main() {
try (var arena = Arena.ofConfined();
var channel = FileChannel.open(path, StandardOpenOption.READ)) {
var size = channel.size();
var memorySegment = channel.map(FileChannel.MapMode.READ_ONLY,
0,
size,
arena
);
System.out.println("成功映射文件,大小:" + printSize(size));
System.out.println("内存段地址:" + memorySegment.address());
System.out.println("内存段大小:" + printSize(memorySegment.byteSize()));
System.out.println("内存段内容前16字节:");
for (long i = 0; i < 16; i++) {
byte b = memorySegment.get(ValueLayout.JAVA_BYTE, i);
System.out.printf("0x%02X ", b);
}
System.out.println();
System.out.println("内存段内容后16字节:");
for (long i = size - 16; i < size; i++) {
byte b = memorySegment.get(ValueLayout.JAVA_BYTE, i);
System.out.printf("0x%02X ", b);
}
System.out.println();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static String printSize(long size) {
String[] units = {"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"};
long s = size;
var list = new ArrayList<String>();
var builder = new StringBuilder();
var formatter = new Formatter(builder);
for (var unit : units) {
int sub = (int) (s & ((1 << 10) - 1));
formatter.format("%s %s", sub, unit);
list.add(builder.toString());
builder.setLength(0);
if (s < 1024) {
break;
}
s >>= 10;
}
return String.join(" ", list.reversed());
}
}
原理说明
mmap 只是建立了虚拟内存地址到磁盘文件的映射,并没有真正加载数据(Lazy Loading)。只有真正读取数据时,OS 才会发生缺页中断(Page Fault)去加载数据。
类似应用
- 数据库系统:SQLite、MySQL、Redis
- Kafka
- RocketMQ
已知缺点
- 本质是在借用 OS 的 Page Cache,如果同时运行数据库(MySQL/Redis)等应用,java 使用 mmap 疯狂读取大文件可能会把数据库在 Page Cache 里的热数据挤出去(Eviction)。
结尾
本文只是提出一种新版本 jdk 的全新 mmap 使用方式,与其他相对较老的方式相比,这种方式可以直接操纵堆外内存,且性能更高,还支持随机读取,唯一缺点可能就是 api 太新,用起来可能需要重新研究写法。
到此这篇关于java中使用mmap技术的实现示例的文章就介绍到这了,更多相关java使用mmap技术内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
