阅读 242

对 DirectByteBuffer解读

更多文章,关注:liangye-xo.xyz

对 DirectByteBuffer解读

浅谈

NIO离不开 DirectByteBuffer,若能在一开始就去合理地去使用 DirectByteBuffer,能够减少一次用户态与内核态数据之间的拷贝,这一点想必是耳熟能详的;但 DirectByteBuffer本身是堆外内存,其不受 gc所管控,但堆外内存也是需要被回收的,否则就会造成 内存泄漏,那 jvm是如何来处理这个问题的呢?

先来看个 demo:TestPhantomReference

public class TestPhantomReference {
    public static void main(String[] args) {
        // 创建引用队列
        ReferenceQueue queue = new ReferenceQueue();
        byte[] buf = new byte[1024 * 1024 * 10];
        // 传入 ReferenceQueue
        PhantomReference phantomReference = new PhantomReference(buf, queue);
        // 置空强引用
        buf = null;
        System.out.println("执行gc前, queue中是否有数据?" + " " + (queue.poll() == null ? "没有" : "有"));
        System.out.println("执行gc前, ref中引用对象: " + phantomReference.get());
        // 发生 gc后, buf对应的对内存会被回收 - JVM本身是认识 Reference的
        // 在进行可达性分析时, 若是有 Reference引用间接指向某块堆内存, JVM会将其忽略, 即 Reference去引用其他堆内存不算是强引用
        // 在发生 gc时, 该被回收的话还是会被回收的
        System.gc();
        System.out.println("执行gc后, ref中引用对象: " + phantomReference.get()); // 对于 PhantomReference.get(), 始终返回的会是 null
        System.out.println("queue中获取的 ref和 weakRef是否一致: " + (queue.poll() == phantomReference ? true : false));
    }

}
复制代码

运行结果:

执行gc前, queue中是否有数据? 没有
执行gc前, ref中引用对象: null
执行gc后, ref中引用对象: null
queue中获取的 ref和 weakRef是否一致: true
复制代码

从执行结果上看,不难分析出 ReferenceQueue的作用:当 Reference中关联对象被回收时, Reference实例对应引用会被加入到引用队列中去

因此,我们可以通过引用队列来去检查 Reference关联的对象是否有被 gc,当 Reference关联对象被 gc时, Reference引用会被加入到引用队列中去,基于此, 当我们发现引用队列中有数据时, 便可以去获取 Reference去做些其它事情了

对构造的解读

    DirectByteBuffer(int cap) {                   // package-private

        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 - 保存创建的堆外内存对应的虚拟内存地址
            // allocateMemory 本地方法, 实际上触发系统调用去分配内存
            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: DirectByteBuffer实例
        // 参数 2: Deallocator实例
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
复制代码

构造中使用 Unsafe去分配了堆外内存,Unsafe本身也是可以去释放堆外内存的

最终都会对应去调用 native方法

    private native long allocateMemory0(long bytes);
    private native void freeMemory0(long address);
复制代码

在构造中为 Cleaner分配了内存,进行相关初始化

创建 Cleaner时,传递了参数:this (DirectByteBuffer)、Deallocator实例自身 - 可以理解为 Deallocator是内存释放器

    // Deallocator - 内存释放器
    private static class Deallocator
        implements Runnable
    {
复制代码

Deallocator实现了 Runnable,对应其 run:

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 通过 Unsafe调用本地方法 freeMemory去释放堆外内存
            UNSAFE.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
复制代码

可以看出,在 run()中通过调用 Unsafe.freeMemory()去释放了堆外内存

不难猜想,DirectByteBuffer对应的堆外内存的释放就是通过 Deallocator来实现的,这其中的逻辑又是怎么整合起来的呢?

继续看:

Cleaner是 DirectByteBuffer的一个字段,实现了虚引用

   // Cleaner 继承了虚引用
    private final Cleaner cleaner;
复制代码
// 继承了虚引用 - PhantomReference
public class Cleaner
    extends PhantomReference<Object>
{
    // Cleaner自己来创建了 引用队列
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
复制代码

Cleaner构造

    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }

	private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        // 将 Deallocator实例引用保存到了 thunk中去
        this.thunk = var2;
    }
复制代码

将 Deallocator实例引用保存到了 thunk中去

在创建 cleaner时,传递了 this和 Deallocator实例,而 DirectByteBuffer在被回收后 (即 Ref关联对象),cleaner会被加入到队列中去,一开始是 Reference.pending队列中去

光从构造中似乎看不出,但可以猜出其实现:依靠于 Reference!

对 Reference解读

Ref状态变迁如下:

字段

    // 存放真实对象引用
    private T referent;         /* Treated specially by GC */

    // 引用队列, 外部可以通过传递引用队列, 方便后续判断 Ref关联对象 referent是否有被 gc回收掉
    volatile ReferenceQueue<? super T> queue;

    // 保存引用队列中其下一个元素的引用 - next构造 RefQueue单向链表
    @SuppressWarnings("rawtypes")
    volatile Reference next;

    // vm线程在判定当前 ref关联 obj是垃圾后, 会将当前 ref加入到 pending队列、
    // pending队列是一个单向链表, 使用 discovered 连接起来
    private transient Reference<T> discovered;
复制代码
    // pending 链表的头部字段
    // pending链表头部的追加字段, 是由 jvm垃圾收集器线程进行追加的
    private static Reference<Object> pending = null;
复制代码

构造

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
复制代码

初始化

在加载 Reference时,会去执行一块静态代码:

    // 加载 Reference时执行
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 创建了 ReferenceHandler
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY); // 最高优先级
        handler.setDaemon(true); // 守护线程
        // 关键
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean waitForReferenceProcessing()
                throws InterruptedException
            {
                return Reference.waitForReferenceProcessing();
            }

            @Override
            public void runFinalization() {
                Finalizer.runFinalization();
            }
        });
    }
复制代码

在初始化代码块中,创建了 ReferenceHandler,并去执行了其 start()

ReferenceHandler是什么?

    private static class ReferenceHandler extends Thread {
        // 加载
        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                // 这里来保证 Cleaner类已经被 jvm加载过了的
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }
        // 初始化时执行
        static {
            // pre-load and initialize Cleaner class so that we don't
            // get into trouble later in the run loop if there's
            // memory shortage while loading/initializing it lazily.
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, null, name, 0, false);
        }

        public void run() {
            while (true) {
                // 消费 Pending队列中元素
                processPendingReferences();
            }
        }
    }
复制代码

初始化 Reference时,当前线程 (ReferenceHandler)会去消费 pending队列中元素

    static boolean tryHandlePending(boolean waitForNotify) {
        // 保存当前线程想要去消费 Ref的引用
        Reference<Object> r;
        // 关键
        Cleaner c;
        try {
            // 这里为什么需要同步 ?
            // 1.jvm垃圾收集器线程需要向 pending队列追加 ref
            // 2.当前线程消费 pending队列
            synchronized (lock) {
                if (pending != null) {
                    // 获取 pending队列头元素
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    // c 一般情况下是 null, 当 r指向的 ref实例时 cleaner实例时, c才会不为 null, 并且去指向 cleaner对象
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    // 出队逻辑
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    // 为了避免线程不断轮询 - 做无用功
                    if (waitForNotify) {
                        // 1.释放锁
                        // 2.阻塞当前线程, 直到其它线程调用了当前 lock.notify() | lock.notifyAll().
                        lock.wait();
                        // 唤醒当前消费线程是谁 ? - jvm垃圾回收线程, 添加 ref到 pending后, 会去调用 lock.notify()
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        // true - 当前 ref是 clean类型实例, 就不会去执行 ref的入队逻辑了.
        if (c != null) {
            c.clean(); // 直接来执行了 Cleaner.clean()。
            return true;
        }
        // 获取 ref中关联的引用队列
        ReferenceQueue<? super Object> q = r.queue;
        // true - 创建 ref时指定了 refQueue - 执行 queue入队逻辑
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }
复制代码

可以看出,当线程在消费 pending队列中元素 (Ref引用)时,若元素不是 Cleaner实例,且其关联了 ReferenceQueue,会被加入到 ReferenceQueue中去,并设置了下状态;当元素是 Cleaner实例时,直接就去执行了 Cleaner.clean()

    public void clean() {
        if (!remove(this))
            return;
        try {
            // 这里来调用了 Deallocator.run() - 实现了对堆外内存的回收
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
复制代码

可以看到,clean()中调用了 thunk.run(),而 thunk便是创建 cleaner时传递进去的 Deallocator

这样就实现了堆外内存的释放:DirectByteBuffer存在字段 Cleaner,当 DirectByteBuffer被 gc时,jvm垃圾回收线程会将 Cleaner引用加入到 pendding队列中去,当线程 (ReferenceHanlder)去消费 pendding中元素时,检测发现其是 Cleaner类实例,此时不会去将该引用添加到 RefQueue中,而是直接去执行了 Cleaner.clean(),在里头调用了 this.chunk.run(),对应的便是 Deallocator.run(),在这里头去释放了堆外内存!

至此,DirectByteBuffer内存释放解读完毕!

文章分类
后端
文章标签