Java零拷贝四步曲——HeapByteBuffer与DirectByteBuffer

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的构造函数中,有两处关键代码:

  1. base = unsafe.allocateMemory(size),通过Unsafe类调用native方法进行内存分配;
  2. 创建了一个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;
}
复制代码
  1. 将返回的地址赋值给了base;
  2. setMemory初始化内存区域;
  3. 最后进行页对齐赋值给了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;
}
复制代码
  1. 赋值address
  2. 创建Cleaner

ByteBuffer.allocateDirec和FileChannel.map都是创建了一个DirectByteBuffer,那它们有什么不同之处呢?它们的不同需要从两方面分析

  1. DirectByteBuffer与MappedByteBuffer MappedByteBuffer是DirectByteBuffer的父类,MappedByteBuffer中封装了文件描述符FileDescriptor fd,前面的文章我们也说过MappedByteBuffer类是Java层提供给开发人员对文件映射内存访问和操作的统一视图,它适用于访问磁盘上文件的场景; 而DirectByteBuffer适用于Java应用层创建的直接内存;
  2. DirectByteBuffer与MappedByteBuffer mmap和malloc的分配有何不同,要回答这个问题,需要介绍一下Linux内存的分配,我将在下一篇文章中讲解。
小结一下

讲到这里,其实从linux底层到Java层的零拷贝基本上都讲完了,还剩下三个扫尾的内容:

  • Java层HeapByteBuffer与Linux底层之前为什么需要DirectByteBuffer?
  • DirectByteBuffer的回收?
  • mmap和malloc的分配有何不同?

为了保证每篇文章只讲清楚一到两个知识点,并且结构清晰,所以上面的三个问题在后续文章中解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值