HeapByteBuffer与DirectByteBuffer
Nio中Buffer类继承图如下,其中最主要的类是HeapByteBuffer和DirectByteBuffer
HeapByteBuffer(堆内内存):顾名思义,是写在jvm堆上面的一个buffer,底层本质是一个数组; 由于内容维护在jvm里,所以把内容写进buffer里速度会快些;并且Java堆内存的管理,是由gc去管理的,更简洁;
DirectByteBuffer(堆外内存):底层的数据其实是维护在内核缓存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据; 由于DirectByteBuffer分配与native memory中,不在heap区,所以不会受到heap区的gc影响,但分配和释放需要更多的成本;
HeapByteBuffer与DirectByteBuffer的创建,都是通过ByteBuffer中的方法来创建的:
public static ByteBuffer allocate(int capacity)
public static ByteBuffer allocateDirect(int capacity)
复制代码
HeapByteBuffer源码
ByteBuffer.allocate方法
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
复制代码
直接调用new HeapByteBuffer
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
}
复制代码
上图创建了一个字节数组并调用其父类的构造方法
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
复制代码
这个创建的数据最终赋值给父类ByteBuffer中的hb变量。所以HeapByteBuffer本质上内部维护的是一个字节数组。
DirectByteBuffer源码
ByteBuffer.allocateDirect方法
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
复制代码
DirectByteBuffer的构造函数中,有两处关键代码:
- base = unsafe.allocateMemory(size),通过Unsafe类调用native方法进行内存分配;
- 创建了一个Deallocator实例,并利用这个实例构造了Cleaner实例,前面我们说过堆外内存的回收都不受JVM管控的,所以这个Cleaner就是负责DirectByteBuffer的回收工作的;
DirectByteBuffer的回收我们后面也会详细讲解的,继续看allocateMemory方法(Unsafe源码需要到openjdk中看),在openjdk的Unsafe.java类中allocateMemory是一个native中法,需要继续查看Unsafe.cpp,发现allocateMemory被定义为Unsafe_AllocateMemory
{CC"allocateMemory", CC"(J)"ADR, FN_PTR(Unsafe_AllocateMemory)},
复制代码
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == 0) {
return 0;
}
sz = round_to(sz, HeapWordSize);
void* x = os::malloc(sz, mtInternal);
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);
复制代码
通过os::malloc调用底层的malloc方法进行内存分配,并返回分配的地址。
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);//初始化内存区域
//进行页对齐
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
复制代码
- 将返回的地址赋值给了base;
- setMemory初始化内存区域;
- 最后进行页对齐赋值给了address;address是父类Buffer中的一个字段;
到这里我们看到address是通过调用底层glibc的malloc方法进行的内存分配,但是在“Java零拷贝三步曲——Java层的实现”一文中,“Java层mmap”的实现address是通过调用系统底层的mmap分配的内存,最终赋值给address;
我们看看FileChannel中的map方法是如何创建java.nio.DirectByteBuffer的
public MappedByteBuffer map(MapMode mode, long position, long size)
复制代码
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
jint fd = fdval(env, fdo);
int protections = 0;
int flags = 0;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
mapAddress = mmap64(
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
if (mapAddress == MAP_FAILED) {
if (errno == ENOMEM) {
JNU_ThrowOutOfMemoryError(env, "Map failed");
return IOS_THROWN;
}
return handle(env, -1, "Map failed");
}
return ((jlong) (unsigned long) mapAddress);
}
复制代码
mmap64进行了文件内存映射,得到mapAddress地址
static MappedByteBuffer newMappedByteBuffer(int size, long addr,
FileDescriptor fd,
Runnable unmapper)
{
MappedByteBuffer dbb;
if (directByteBufferConstructor == null)
initDBBConstructor();
try {
dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
new Object[] { new Integer(size),
new Long(addr),
fd,
unmapper });
} catch (InstantiationException |
IllegalAccessException |
InvocationTargetException e) {
throw new InternalError(e);
}
return dbb;
}
复制代码
然后通过newInstance创建了一个DirectByteBuffer实例
protected DirectByteBuffer(int cap, long addr,
FileDescriptor fd,
Runnable unmapper)
{
super(-1, 0, cap, cap, fd);
address = addr;
cleaner = Cleaner.create(this, unmapper);
att = null;
}
复制代码
- 赋值address
- 创建Cleaner
ByteBuffer.allocateDirec和FileChannel.map都是创建了一个DirectByteBuffer,那它们有什么不同之处呢?它们的不同需要从两方面分析
- DirectByteBuffer与MappedByteBuffer MappedByteBuffer是DirectByteBuffer的父类,MappedByteBuffer中封装了文件描述符FileDescriptor fd,前面的文章我们也说过MappedByteBuffer类是Java层提供给开发人员对文件映射内存访问和操作的统一视图,它适用于访问磁盘上文件的场景; 而DirectByteBuffer适用于Java应用层创建的直接内存;
- DirectByteBuffer与MappedByteBuffer mmap和malloc的分配有何不同,要回答这个问题,需要介绍一下Linux内存的分配,我将在下一篇文章中讲解。
小结一下
讲到这里,其实从linux底层到Java层的零拷贝基本上都讲完了,还剩下三个扫尾的内容:
- Java层HeapByteBuffer与Linux底层之前为什么需要DirectByteBuffer?
- DirectByteBuffer的回收?
- mmap和malloc的分配有何不同?
为了保证每篇文章只讲清楚一到两个知识点,并且结构清晰,所以上面的三个问题在后续文章中解决。