Java零拷贝续——DirectByteBuffer内存回收

2,223 阅读3分钟
  • DirectByteBuffer中的address指向的地址空间属于堆外内存,它是不受JVM的管控,所以需要我们自己去管理;
  • 前面的文章我们分析过address的地址由mmap或malloc的分配,那么其实DirectByteBuffer回收也是分为两处;

回收malloc分配的内存

当我们直接调用如下代码创建DirectByteBuffer,其内部的address由malloc分配(如果平台支持直接内存)

ByteBuffer.allocateDirect(1024)

构造方法

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 = 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;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

Cleaner其实是一个虚引用,Deallocator是一个Runnable

public class Cleaner extends PhantomReference<Object>

回收的核心原理是虚引用,用于跟踪垃圾回收过程,可以参考我的“Java、Android引用类型”文章。

 private Cleaner(Object referent, Runnable thunk) {
    super(referent, dummyQueue);
    this.thunk = thunk;
}
  • referent:代表我们创建的DirectByteBuffer;
  • dummyQueue:为Cleaner内部维护的引用队列;
    我们直接看父类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");
    /* 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 tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

父类静态代码块创建了一个守护线程,当DirectByteBuffer对象从pending状态 ——> enqueue状态时,在handler的run方法中会检测到

 static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    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) {
                        lock.wait();
                    }
                    // 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
        if (c != null) {
            c.clean();
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

如果是c = r instanceof Cleaner ? (Cleaner) r : null;就会得到一个Cleaner,并且调用它的c.clean()方法,

public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }

clean()方法最终回调用我们初始化时传进来的Deallocator

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

在Deallocator中调用unsafe.freeMemory(address);进行回收

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

回收mmap分配的内存

我们看一次FileChannleImpl中map方法代码

int isize = (int)size;
            Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
            if ((!writable) || (imode == MAP_RO)) {
                return Util.newMappedByteBufferR(isize,
                                                 addr + pagePosition,
                                                 mfd,
                                                 um);
            } else {
                return Util.newMappedByteBuffer(isize,
                                                addr + pagePosition,
                                                mfd,
                                                um);
            }

Util.newMappedByteBuffer调用用传入了一个Unmapper实例,Unmapper其实也是一个Runnable,到此其实大家也明白了,最终的回收还是在Unmapper中完成的

public void run() {
    if (address == 0)
        return;
    unmap0(address, size);
    address = 0;

    // if this mapping has a valid file descriptor then we close it
    if (fd.valid()) {
        try {
            nd.close(fd);
        } catch (IOException ignore) {
            // nothing we can do
        }
    }

    synchronized (Unmapper.class) {
        count--;
        totalSize -= size;
        totalCapacity -= cap;
    }
}

最后调用底层的unmap0来完成内存的回收。