支撑百万并发的“零拷贝”技术,你了解吗?( 九 )

  • MAP_FIXED:不建议使用,这种模式下 addr 参数指定的必须提供一个存在的 addr 参数 。
  • fd:文件描述符 。每次 map 操作会导致文件的引用计数加 1,每次 unmap 操作或者结束进程会导致引用计数减 1 。
    offset:文件偏移量 。进行映射的文件位置,从文件起始地址向后的位移量 。
    下面总结一下 MappedByteBuffer 的特点和不足之处:
    • MappedByteBuffer 使用是堆外的虚拟内存,因此分配(map)的内存大小不受 JVM 的 -Xmx 参数限制,但是也是有大小限制的 。
    • 如果当文件超出 Integer.MAX_VALUE 字节限制时,可以通过 position 参数重新 map 文件后面的内容 。
    • MappedByteBuffer 在处理大文件时性能的确很高,但也存在内存占用、文件关闭不确定等问题,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的 。
    • MappedByteBuffer 提供了文件映射内存的 mmap() 方法,也提供了释放映射内存的 unmap() 方法 。然而 unmap() 是 FileChannelImpl 中的私有方法,无法直接显示调用 。
    因此,用户程序需要通过 Java 反射的调用 sun.misc.Cleaner 类的 clean() 方法手动释放映射占用的内存区域 。
    1. public static void clean(final Object buffer) throws Exception {
    2. AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
    3. try {
    4. Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
    5. getCleanerMethod.setAccessible(true);
    6. Cleaner cleaner = (Cleaner) getCleanerMethod.invoke(buffer, new Object[0]);
    7. cleaner.clean();
    8. } catch(Exception e) {
    9. e.printStackTrace();
    10. }
    11. });
    12. }
    DirectByteBuffer
    DirectByteBuffer 的对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理 。
    一般使用 DirectByteBuffer 的静态方法 allocateDirect() 创建 DirectByteBuffer 实例并分配内存 。
    1. public static ByteBuffer allocateDirect(int capacity) {
    2. return new DirectByteBuffer(capacity);
    3. }
    DirectByteBuffer 内部的字节缓冲区位在于堆外的(用户态)直接内存,它是通过 Unsafe 的本地方法 allocateMemory() 进行内存分配,底层调用的是操作系统的 malloc() 函数 。
    1. DirectByteBuffer(int cap) {
    2. super(-1, 0, cap, cap);
    3. boolean pa = VM.isDirectMemoryPageAligned();
    4. int ps = Bits.pageSize();
    5. long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    6. Bits.reserveMemory(size, cap);
    7. long base = 0;
    8. try {
    9. base = unsafe.allocateMemory(size);
    10. } catch (OutOfMemoryError x) {
    11. Bits.unreserveMemory(size, cap);
    12. throw x;
    13. }
    14. unsafe.setMemory(base, size, (byte) 0);
    15. if (pa && (base % ps != 0)) {
    16. address = base + ps - (base & (ps - 1));
    17. } else {
    18. address = base;
    19. }
    20. cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    21. att = null;
    22. }
    除此之外,初始化 DirectByteBuffer 时还会创建一个 Deallocator 线程,并通过 Cleaner 的 freeMemory() 方法来对直接内存进行回收操作,freeMemory() 底层调用的是操作系统的 free() 函数 。
    1. private static class Deallocator implements Runnable {
    2. private static Unsafe unsafe = Unsafe.getUnsafe();
    3. private long address;
    4. private long size;
    5. private int capacity;
    6. private Deallocator(long address, long size, int capacity) {
    7. assert (address != 0);
    8. this.address = address;
    9. this.size = size;
    10. this.capacity = capacity;
    11. }
    12. public void run() {
    13. if (address == 0) {
    14. return;
    15. }
    16. unsafe.freeMemory(address);
    17. address = 0;
    18. Bits.unreserveMemory(size, capacity);
    19. }
    20. }
    由于使用 DirectByteBuffer 分配的是系统本地的内存,不在 JVM 的管控范围之内,因此直接内存的回收和堆内存的回收不同,直接内存如果使用不当,很容易造成 OutOfMemoryError 。
    说了这么多,那么 DirectByteBuffer 和零拷贝有什么关系?前面有提到在 MappedByteBuffer 进行内存映射时,它的 map() 方法会通过 Util.newMappedByteBuffer() 来创建一个缓冲区实例 。
    初始化的代码如下:
    1. static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd,
    2. Runnable unmapper) {
    3. MappedByteBuffer dbb;
    4. if (directByteBufferConstructor == null)
    5. initDBBConstructor();
    6. try {
    7. dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
    8. new Object[] { new Integer(size), new Long(addr), fd, unmapper });
    9. } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {


      推荐阅读