美文网首页
ByteBuffer之DirectByteBuffer源码分析

ByteBuffer之DirectByteBuffer源码分析

作者: 无聊之园 | 来源:发表于2019-05-13 09:49 被阅读0次

ByteBuffer的另一个子类DirectByteBuffer,直接在堆外分配内存。

ByteBuffer的allocateDirect方法获得这个对象

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

DirectByteBuffer直接在堆外分配内存,在某些场景的时候,能够省去java堆到内核的内存复制过程,比如socket发送数据的时候,需要先把数据从java堆复制到内核,再发送出去。

DirectByteBuffer源码比HeapByteBuffer复杂
主要是申请堆外内存的过程,也就是构造方法。其他的get put等方法都是native方法,逻辑意义和HeapByteBuffer差不多,研究意义不大。只研究构造方法。

构造方法

DirectByteBuffer(int cap) {                   // package-private
         // 初始化mark,position,limit,capacity
        super(-1, 0, cap, cap);
        // 是否进行页对齐,是的话,则实际申请的内存可能会增加达到对齐效果
        // 默认关闭,可以通过-XX:+PageAlignDirectMemory控制
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        // 页对齐的话,实际申请的内存可能会比cap大,这里不会
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 预定内存,预定不到则进行回收堆外内存,再预定不到则进行Full gc,下文有代码分析
        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;
        }
/*
创建堆外内存回收Cleanner,Cleanner对象是一个PhantomFerence幽灵引用,
DirectByteBuffer对象的堆内存回收了之后,幽灵引用Cleanner会通知Reference
对象的守护进程ReferenceHandler对其堆外内存进行回收,调用Cleanner的
clean方法,clean方法调用的是Deallocator对象的run方法,run方法调用的是
unsafe.freeMemory回收堆外内存。

堆外内存minor gc和full gc的时候都不会进行回收(下文有测试),而是ReferenceHandle守护进程调用
cleanner对象的clean方法进行回收。只不过gc 回收了DirectByteBuffer之后,gc会通知Cleanner进行回收
*
*/
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;



    }
// 预定内存
static void reserveMemory(long size, int cap) {
          // memoryLimitSet初始值为false
          // 获取允许的最大堆外内存赋值给maxMemory,默认是64MB,可以通过-XX:MaxDirectMemorySize参数控制 
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        // 尝试预定堆外内存
// cap <= 最大的堆外内存-已经使用的堆外内存的cap大小,则说明内存足够,否则内存不够,返回false
// 预定成功,则返回
        // optimist!
        if (tryReserveMemory(size, cap)) {
            return;
        }
        // 预定失败,进入这里
        /* 加载Reference类的时候,Reference的静态代码块调用了SharedSecrets的set方法,所以这里获取的就是Reference静态代码块set进去的JavaLangRefAccess
        * SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            public boolean tryHandlePendingReference() {
                return Reference.tryHandlePending(false);
            }
        });
         */
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        /*
尝试进行堆外内存回收,这里调用的实际是Reference.tryHandlePending(false);false代表非堵塞调用,回收不了就返回false
不堵塞, Reference.tryHandlePending(false)实际调用的是Cleaner的clean方法回
收堆外内存, Cleaner是DirectByteBuffer的构造方法创建的:cleaner =
Cleaner.create(this, new Deallocator(base, size, cap));所以Cleaner的clean方法
实际是Deallocator的run方法,run方法执行的是的unsafe.freeMemory回收堆外内存
*/
unsafe.freeMemory(address);
        while (jlra.tryHandlePendingReference()) {
             // 回收了堆外内存之后,再进行预定内存
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }
        // 还分配不到则进行full gc垃圾回收
        // trigger VM's Reference processing
        System.gc();

        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }
// 尝试预定堆外内存
// cap <= 最大的堆外内存-已经使用的堆外内存的cap大小,则说明内存足够,否则内存不够,返回false
private static boolean tryReserveMemory(long size, int cap) {
        // -XX:MaxDirectMemorySize limits the total capacity rather than the
        // actual memory usage, which will differ when buffers are page
        // aligned.
        long totalCap;
        while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
            if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
                 // 已预定内存大小reservedMemory增加size
                reservedMemory.addAndGet(size);
                count.incrementAndGet();
                return true;
            }
        }

        return false;
    }

总结这一过程:
DirectByteBuffer有两部分,一部分就是DirectByteBuffer这个对象,这个对象是在java堆中,另一部分就是其开辟的堆外内存,创建这个对象的时候,会先判断是否有足够的堆外内存空间,如果没有,则尝试进行回收一次堆外内存,回收了之后再预定一次空间,空间还不够的话,则执行System.gc,进行full gc(因为很可能堆内内存还没来得及gc,堆外内存就不够了),full gc回收了DirectByteBuffer对象的堆内内存。因为创建DirectByteBuffer的时候,创建了Cleanner这个幽灵引用,所以,DirectByteBuffer被gc回收了之后,gc会给Reference的pending设置值,ReferenceHandler守护线程会轮询pengding,发现pending不为空,则进行处理,调用Cleanner的clean方法,clean方法会调用Deallocator对象的run方法,run方法执行的是unsafe.freeMemory(address),address是直接内存的内存地址,是unsafe.allocateMemory(size)申请内存的时候返回的。
所以:对于直接内存的回收,只有一个方法unsafe.freeMemory(address)。gc对直接内存的影响是间接的,是触发了Cleanner幽灵引用的clean方法调用了unsafe.freeMemory(address)。

下面做一个测试:测试Full gc不会对直接内存回收。
设计jvm参数 :-verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=40M
最大直接内存40M,打印垃圾回收日志

public class RevisedObjectInHeap
{
    Unsafe unsafe;
    public Unsafe getUnsafe() {
        // 通过反射得到theUnsafe对应的Field对象
        Field field = null;
        try {
            field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置该Field为可访问
            field.setAccessible(true);
            // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
            unsafe = (Unsafe) field.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
    }
    private long address = 0;

    public RevisedObjectInHeap()
    {
        Unsafe unsafe = getUnsafe();
        address = unsafe.allocateMemory(2 * 1024 * 1024);
//        unsafe.freeMemory(address);
//        bytes = new byte[1024 * 1024];
    }

    public static void main(String[] args) throws InterruptedException {
        while (true)
        {
            RevisedObjectInHeap heap = new RevisedObjectInHeap();
            System.gc();
        }
    }

}

结果:垃圾回收一直在循环运行,但是最终还是包OOM错误。但是如果执行unsafe.freeMemory(address)释放内存,则不会报OOM错误。

顺便提一句:FileChannel的map映射也和直接内存有关系。

内存映射的原理都是操作系统支持才可行的:linux和windows都有内存映射机制,大概原理就是,将磁盘的文件和缓冲区建立一个映射关系,然后访问缓冲区就想访问磁盘文件一样,自然,访问缓冲区非常快,所以read和write也非常快。(做过测试,这种方式会快5、6倍)因为磁盘文件和缓冲区不是java堆的数据,所以自然使用直接内存会省去一次赋值,快很多,使用的Buffer是MappedByteBuffer。
大概的代码如下:

public class MapMemoryBuffer {
    public static void main(String[] args) throws Exception {
        RandomAccessFile f = new RandomAccessFile("C:/hinusDocs/hello.txt", "rw");
        FileChannel fc = f.getChannel();
        MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());

        while (buf.hasRemaining()) {
            System.out.print((char)buf.get());
        }
        System.out.println();
    }
}

            MappedByteBuffer var5 = (MappedByteBuffer)directByteBufferConstructor.newInstance(new Integer(var0), new Long(var1), var3, var4);

相关文章

网友评论

      本文标题:ByteBuffer之DirectByteBuffer源码分析

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