美文网首页
Netty系列之Direct Buffers

Netty系列之Direct Buffers

作者: 海外党一枚 | 来源:发表于2021-06-26 12:09 被阅读0次

    1、什么是堆外内存
    堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。

    堆外内存使用Native函数库(通过Unsafe类的allocateMemory()方法申请分配内存,底层会调用操作系统的的malloc函数)直接分配(native堆),然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

    2、堆外内存解决了什么问题
    解决HeapByteBuffer存在的问题:
    如果os和jvm都是用jvm里边的数据区域, 但是jvm会对这块内存区域进行GC回收,可能会对这块内存的数据进行更改,根据我们的假设,由于这块区域os也在使用,jvm对这块共享数据发生了变更,os那边就会出现数据错乱的情况。那么如果不让jvm对这块共享区域进行GC是不是可以避免这个问题呢?答案是不行的,也会存在问题,如果jvm不对其进行GC回收,jvm这边可能会出现OOM的内存溢出。因此只能拷贝jvm的那一份到os的内存空间,即使jvm那边的数据区域被改变,但是os里边的不会受到影响,等os使用io结束后会对这块区域进行回收,因为这是os的管理范围之内。这样就造成性能降低。
    因此,在JDK1.4中新加入了NIO,引入了一种基于通道(Channel)和缓存区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存(native堆),然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

    3、堆外内存的实现
    DirectByteBuffer 对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理,一般使用 DirectByteBuffer 的静态方法 allocateDirect() 创建 DirectByteBuffer 实例并分配内存。

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    

    DirectByteBuffer 内部的字节缓冲区位在于堆外的(用户态)直接内存,它是通过 Unsafe 的本地方法 allocateMemory() 进行内存分配,底层调用的是操作系统的 malloc() 函数。

    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)) {
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
    

    除此之外,初始化 DirectByteBuffer 时还会创建一个 Deallocator 线程,并通过 Cleaner 的 freeMemory() 方法来对直接内存进行回收操作,freeMemory() 底层调用的是操作系统的 free() 函数。

    private static class Deallocator implements Runnable {
        private static Unsafe unsafe = Unsafe.getUnsafe();
    
        private long address;
        private long size;
        private int capacity;
    
        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }
    
        public void run() {
            if (address == 0) {
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
    }
    

    由于使用 DirectByteBuffer 分配的是系统本地的内存,不在 JVM 的管控范围之内,因此直接内存的回收和堆内存的回收不同,直接内存如果使用不当,很容易造成 OutOfMemoryError。

    DirectByteBuffer 和零拷贝有什么关系?

    DirectByteBuffer 是 MappedByteBuffer 的具体实现类。实际上,Util.newMappedByteBuffer() 方法通过反射机制获取 DirectByteBuffer 的构造器,然后创建一个 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;
    }
    

    在 MappedByteBuffer 进行内存映射时,它的 map() 方法会通过 Util.newMappedByteBuffer() 来创建一个缓冲区实例,初始化的代码如下:

    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;
    }
    
    private static void initDBBRConstructor() {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                try {
                    Class<?> cl = Class.forName("java.nio.DirectByteBufferR");
                    Constructor<?> ctor = cl.getDeclaredConstructor(
                        new Class<?>[] { int.class, long.class, FileDescriptor.class,
                                        Runnable.class });
                    ctor.setAccessible(true);
                    directByteBufferRConstructor = ctor;
                } catch (ClassNotFoundException | NoSuchMethodException |
                         IllegalArgumentException | ClassCastException x) {
                    throw new InternalError(x);
                }
                return null;
            }});
    }
    

    因此,除了允许分配操作系统的直接内存以外,DirectByteBuffer 本身也具有文件内存映射的功能。我们需要关注的是,DirectByteBuffer 在 MappedByteBuffer 的基础上提供了内存映像文件的随机读取 get() 和写入 write() 的操作。

    内存映像文件的随机读操作

    public byte get() {
        return ((unsafe.getByte(ix(nextGetIndex()))));
    }
    
    public byte get(int i) {
        return ((unsafe.getByte(ix(checkIndex(i)))));
    }
    

    内存映像文件的随机写操作

    public ByteBuffer put(byte x) {
        unsafe.putByte(ix(nextPutIndex()), ((x)));
        return this;
    }
    
    public ByteBuffer put(int i, byte x) {
        unsafe.putByte(ix(checkIndex(i)), ((x)));
        return this;
    }
    

    内存映像文件的随机读写都是借助 ix() 方法实现定位的, ix() 方法通过内存映射空间的内存首地址(address)和给定偏移量 i 计算出指针地址,然后由 unsafe 类的 get() 和 put() 方法和对指针指向的数据进行读取或写入。

    private long ix(int i) {
        return address + ((long)i << 0);
    }
    

    相关文章

      网友评论

          本文标题:Netty系列之Direct Buffers

          本文链接:https://www.haomeiwen.com/subject/bmvddltx.html