NIO的堆外内存-如何分配和回收

1,586 阅读3分钟

1.使用场景

Netty:

  • Unpooled.directBuffer()执行时,会以256字节(AbstractByteBufAllocator.DEFAULT_INITIAL_CAPACITY)调用nio的ByteBuffer.allocateDirect(int capacity)方法。
  • ByteBuffer.allocateDirect中创建了DirectByteBuffer对象.

2.DirectByteBuffer

    DirectByteBuffer(int cap) {

        super(-1, 0, cap, cap);
        // 是否需要页对齐
        boolean pa = VM.isDirectMemoryPageAligned();
        // 每个页的大小
        int ps = Bits.pageSize();

        // 如果需要对齐,则多申请一页。最小1字节
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));

        // 1.检查堆外内存,并记录直接内存
        Bits.reserveMemory(size, cap);

        // 内存基址
        long base = 0;
        try {
            // 2.按照size字节开辟内存空间,并返回内存基址。
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        // 3.内存区域初始化为0
        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;
        }
        // 4.创建Cleaner
        /* Cleaner是一个PhantomReference(虚引用)
           Cleaner构造函数接受一个Runnable参数,在执行clean()的时候做清理
           这里传入了一个Deallocator的Runnable对象,执行unsafe.freeMemory清理内存
        */ 
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

3.Bits.reserveMemory

  • 调用的tryReserveMemory方法,尝试计算可以使用的直接内存。
  • 默认的最大直接内存:sun.misc.VM.directMemory = 67108864L,即64M。
  • tryHandlePendingReference调用sun.misc.Cleaner.clean()方法清理内存,最终是在Deallocator.run中调用unsafe.freeMemory释放内存。
  • 如果启动时加上-XX:+DisableExplicitGC,则System.gc()不会起作用。所以用到堆外内存的应用,不应该加此参数。
static void reserveMemory(long size, int cap) {

        if (!memoryLimitSet && VM.isBooted()) {
            /*
            获取最大直接内存
            如果没有通过-XX:MaxDirectMemorySize指定,则使用67108864字节,即64M作为最大直接内存。
            */
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // 1. 尝试计算内存是否充足
        if (tryReserveMemory(size, cap)) {
            return;
        }

        /*
         在Reference的静态代码中:
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
        */
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
       
       // 2. 调用所有Cleaner.clean()方法
        while (jlra.tryHandlePendingReference()) {
            // 尝试计算内存是否充足
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // 3. 触发gc
        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) {
                // 4. 尝试计算内存是否充足
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                // MAX_SLEEPS = 9
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                // 5. 调用所有Cleaner.clean()方法
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        // 9次循环,sleep时间分别为 1, 2, 4, 8, 16, 32, 64, 128, 256毫秒。
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // 抛出OOM
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

4.Cleaner

sun.misc.CleanerPhantomReference的子类,是一个链表

  • 三个属性:
    // 下一个元素
    private Cleaner next = null;
    // 上一个元素
    private Cleaner prev = null;
    // clean()时执行的任务
    private final Runnable thunk;
  • 两个静态变量:
    // 引用队列,在执行静态方法create的时候,用来设置Reference.queue。
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    // 链表的头元素,在clean()中移除当前元素时,会从这里开始找。
    private static Cleaner first = null;
  • 创建:
    // 私有构造函数创建对象,并且加到静态变量first的队尾。
    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
  • clean方法:
    public void clean() {
        if (remove(this)) { // 从first中移除当前对象
            try {
                // 执行清理任务,即DirectByteBuffer中创建的Deallocator对象
                this.thunk.run();
            } catch (final Throwable var2) {
               ....
            }

        }
    }
  • 在哪里调用?在Reference.tryHandlePending中。

5.Reference.tryHandlePending

tryHandlePending是Reference中一个核心的静态方法,用来处理pending状态的引用:

static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 当前Reference对象是不是Cleaner对象。
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // 省略代码
                } else {
                    // 省略代码
                }
            }
        } catch (OutOfMemoryError x) {
            // 省略代码
        }

        if (c != null) {
            // 执行Cleaner.clean()
            c.clean();
            return true;
        }
        // 省略代码
        return true;
    }

有两个地方调用:

  • 1 Reference的静态代码块中启动的线程ReferenceHandler中:
    private static class ReferenceHandler extends Thread {
        // 省略代码
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }

    // Reference的静态代码块
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
        
        // 省略代码
    }
  • 2 Reference的静态代码块中提供的JavaLangRefAccess的实现中;
    static {
        // 省略代码
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

这个实现会被其他类调用,比如Bits.reserveMemory中的final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

6.Deallocator

  • 1 实现了Runnable
  • 2 在run中通过unsafe.freeMemory等释放内存。
   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) {
                // Paranoia
                return;
            }
            // 释放内存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

总结

  • 1 通过unsafe.allocateMemory和unsafe.setMemory开辟堆外内存空间并初始化;
  • 2 通过unsafe.freeMemory回收内存;
  • 3 通过Bits.reserveMemory尝试保留内存空间,不足时通过Reference.tryHandlePending,System.gc()等方式尝试释放内存;
  • 4 使用Cleaner,在DirectByteBuffer对象回收时,进行堆外内存的回收(Deallocator);
  • 5 尝试分配堆外内存时,Bits.reserveMemory会调用System.gc(),所以在有堆外内存的场景下,不建议添加-XX:-+DisableExplicitGC,否则System.gc()会失效。