返回顶部

DirectByteBuffer直接内存(堆外内存)是怎么被回收的?

DirectByteBuffer,即指向堆外内存,如果要释放它的内存,是不能通过 Jvm 垃圾回收器来回收(垃圾回收器只能回收堆内内存),需要通过虚引用的方式。

一直以来,都只是知道是通过虚引用,但是到底具体怎么回收的,不得而知。最近听黄老师课并翻了代码,才终于清晰。

以下是个人总结。

在 DirectByteBuffer 类文件中,可以看到其内部有 Deallocator 和 Cleaner 两个类的声明,而这两个类就是跟内存的回收有重要的关系。

堆外内存仍然属于用户空间并且属于 JVM 进程的一部分。

一、Deallocator 和 Cleaner

1、Deallocator

第一个重要的类,Deallocator。

Deallocator 是一个 Runnable,那么自然就关注它的 run 方法。在 run 方法里面,调用了 unsafe.freeMemory 方法来达到释放内存的目的。

因为分配内存用的是 unsafe.setMemory,那么对应的释放也会是调用 unsafe 类的方法。

image-20221019213037317

那么接下来的问题就是 Deallocator 是被谁调用来开启 run 方法的。

那答案就是 Cleaner 了。

2、Cleaner

从下面的有注释的代码可以看出,

(1) Cleaner 继承自 PhantomReference 虚引用;

(2) 持有一个 Runnable 对象,在 clean() 方法中会调用该 Runnable 的 run() 方法。

刚刚上面的 Deallocator 这个类正好就是个 Runnable。

没错,Cleaner 中的 Runnable thunk 指向Deallocator 。在 new DirectByteBuffer 时调用的构造方法中,就会创建 cleaner 对象。

image-20221019213633879

所以在调用 Cleaner.clean() 时会调用 Deallocator的run()方法,从而回收堆外内存。

那么接下来问题就变成 Cleaner的clean方法又是什么时候被调用的?

//(只摘录了部分代码)
public class Cleaner extends PhantomReference<Object> { // !!! 继承 虚引用
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;  //!!! 持有一个Runnable对象


    public void clean() {
        if (remove(this)) {
            try {
                this.thunk.run();  //!!! 在这里调用了run方法
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

Cleaner 继承PhantomReference,PhantomReference又继承自Reference。Reference中有一个重要的类为ReferenceHandler。

这个类是个守护线程,一直伴随主线程。它就一直循环执行一个方法tryHandlePending()。

在这个tryHandlePending()中,会拿到pending对象,从而拿到对应的cleaner对象。

然后就会调用cleaner.clean()方法,其中再Deallocator的run(),然后再调用unsafe.freeMemory。

static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) { //当Cleaner持有的directByteBuffer对象被回收时,JVM会把该Cleaner赋值给pending
                    r = pending;  
                    c = r instanceof Cleaner ? (Cleaner) r : null; //所以这里拿到pending后,也就会拿到cleaner对象
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                   
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
           
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }

        if (c != null) {
            c.clean();  //!!! 在这里调用到了 cleaner.clean()方法
            return true;
        }

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

二、直接内存回收流程总结

业务代码 将 DirectByteBuffer 置为null,表示想要回收这块指向的堆外内存;

JVM垃圾回收器检测到该 DirectByteBuffer 对象不可达,将其回收,然后将它对应的虚引用对象 Cleaner 放到 Reference 的 pending 属性中;

后台守护线程 ReferenceHandler 执行 tryHandlePending() 方法。检测到 pending 属性不为空,则拿到 Cleaner 对象,然后调用 Cleaner 对象的 clean 方法;

在 Cleaner 对象的 clean() 方法中,会调用 DirectByteBuffer 的内部类 Deallocator 的 run() 方法;

在 run 方法中,会调用 unsafe.freeMemory() 方法,从而释放了堆外内存。

posted @ 2024-03-29 21:36  四十万尺的菲林  阅读(278)  评论(0)    收藏  举报