MappedByteBuffer 简称为 MBB。 Mapped Byte Buffer = 映射 字节 缓冲区。
参考链接
JDK核心JAVA源码解析(4) - 堆外内存、零拷贝、DirectByteBuffer以及针对于NIO中的FileChannel的思考
主要作用
A direct byte buffer whose content is a memory-mapped region of a file.
一个直接字节缓冲区,它的内容是文件内存映射区域。
优势
本来我们需要操作FileChannel和ByteBuffer的交互来实现文件读写,有了 MBB,可以直接对文件进行操作。
将磁盘上的文件映射为内存物理空间的地址,减少了操作系统缓冲区复制带来的性能开销。
继承结构
父子关系
由于 MBB 是一个抽象类,所以不能通过 new 的方式来获得实例。
public abstract class MappedByteBuffer extends ByteBuffer
从FileChannel中获得 MBB
public abstract MappedByteBuffer map(MapMode mode,long position, long size) throws IOException;
实际上通过map方法获得的是 DirectByteBuffer,根据继承关系知道 DBB 是 MBB 的子类。
MapMode
public static class MapMode {
public static final MapMode READ_ONLY
= new MapMode("READ_ONLY");
public static final MapMode READ_WRITE
= new MapMode("READ_WRITE");
public static final MapMode PRIVATE
= new MapMode("PRIVATE");
//省略.....
}
有三种权限,只读,读写,私有。
公共方法
isLoaded
true if it is likely that this buffer’s content is resident in physical memory
如果此缓冲区的内容可能驻留在物理内存中返回true。
参考案例1,即使load仍然可能为false;
force
Forces any changes made to this buffer’s content to be written to the storage device containing the mapped file.
将缓冲区的内容强制写入到文件中。
load
Loads this buffer’s content into physical memory.
将此缓冲区的内容加载到物理内存中。
此方法尽最大努力确保在返回时,此缓冲区的内容驻留在物理内存中。
调用此方法可能会导致一些页面错误和I / O操作发生。
slice
Creates a new byte buffer whose content is a shared subsequence of this buffer’s content.
This method makes a best effort(尽最大努力) to ensure that, when it returns, this buffer’s content is resident in physical memory.
源码
public ByteBuffer slice() {
int pos = this.position();
int lim = this.limit();
assert (pos <= lim);
//pos <= lim rem = lim -pos
//pos > lim rem = 0
int rem = (pos <= lim ? lim - pos : 0);
//off = pos << 0
int off = (pos << 0);
assert (off >= 0);
return new DirectByteBuffer(this, -1, 0, rem, rem, off);
}
duplicate
Creates a new byte buffer that shares this buffer’s content.
源码
public ByteBuffer duplicate() {
return new DirectByteBuffer(this, this.markValue(), this.position(),
this.limit(), this.capacity(), 0);
}
slice和duplicate区别
ByteBuffer slice = mbb.slice();
ByteBuffer duplicate = mbb.duplicate();
从变量来看,他们三个都是独立的对象
从源码来看,duplicate就是创建了一个新DBB,并且指针跟原来的一样。slice是做了一个切分。
我们用一个案例来看一下具体的效果。
try (
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fc = raf.getChannel();
) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
//第一行输出
System.out.println(mbb.toString());
//从第10个位置开始往后读
fc.read(mbb,10);
//第二行输出
System.out.println(mbb.toString());
ByteBuffer slice = mbb.slice();
ByteBuffer duplicate = mbb.duplicate();
//slice输出
System.out.println(slice.toString());
//duplicate输出
System.out.println(duplicate.duplicate());
输出
java.nio.DirectByteBuffer[pos=0 lim=36 cap=36]
java.nio.DirectByteBuffer[pos=26 lim=36 cap=36]
java.nio.DirectByteBuffer[pos=0 lim=10 cap=10]
java.nio.DirectByteBuffer[pos=26 lim=36 cap=36]
总结:本质上都是创建一个新的缓冲区,区别是三个指针的位置。复制是完全不变,slice是 limit - position 的一段缓冲区。
DirectByteBufferR
public ByteBuffer asReadOnlyBuffer() {
return new DirectByteBufferR(this,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
0);
}
当对buffer指定asReadOnlyBuffer只读buf后,会新建一个DirectByteBufferR,后面这个R就是只读的意思。
RocketMQ
另外,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。
在 RocketMQ 中搜索MappedByteBuffer发现以下用法
下面我们一个个类来归纳用法
MappedFile
主要方法:slice,force,clean(mbb)
StoreCheckpoint
主要方法getLong,clean,putLong,force
StoreTestUtil
IndexFile
clean
案例1 使用 force 写入 uuid
public class MappedByteBuffer01 {
static File file = new File("D://a.txt");
public static void main(String[] args) throws Exception {
try (
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fc = raf.getChannel();
) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
System.out.println("init mbb = " + mbb.toString());
boolean loaded = mbb.isLoaded();
System.out.println("before load = " + loaded);
if (!loaded) {
mbb.load();
loaded = mbb.isLoaded();
System.out.println("after load = " + loaded);
System.out.println("load mbb = " + mbb);
//写入 uuid
String uuid = UUID.randomUUID().toString();
System.out.println("uuid = " + uuid);
byte[] bytes = uuid.getBytes(StandardCharsets.UTF_8);
mbb.put(bytes);
mbb.force();
System.out.println("force mbb = " + mbb);
mbb.flip();
System.out.println("flip mbb = " + mbb);
byte[] readBytes = new byte[uuid.length()];
mbb.get(readBytes);
System.out.println("get mbb = " + mbb);
System.out.println(new String(readBytes));
}
}
}
}
输出
init mbb = java.nio.DirectByteBuffer[pos=0 lim=36 cap=36]
before load = false
after load = false
load mbb = java.nio.DirectByteBuffer[pos=0 lim=36 cap=36]
uuid = ac84715f-2b3e-4c94-b02b-7b0da1c10142
force mbb = java.nio.DirectByteBuffer[pos=36 lim=36 cap=36]
flip mbb = java.nio.DirectByteBuffer[pos=0 lim=36 cap=36]
get mbb = java.nio.DirectByteBuffer[pos=36 lim=36 cap=36]
ac84715f-2b3e-4c94-b02b-7b0da1c10142
结论:即使 load方法调用过isLoaded仍然可能返回false;force会将buf中的内容写入文件,并且可以flip后把buf中的内容读出来。