Netty源碼—7、內存釋放

Netty自己在內存分配上支持堆內存和直接內存,咱們通常選用直接內存,這也是默認的配置。因此要理解Netty內存的釋放咱們得先看下直接內存的釋放。java

Java直接內存釋放

咱們先來看下直接內存是怎麼使用的jvm

ByteBuffer.allocateDirect(capacity)

申請的過程是其實就是建立一個DirectByteBuffer對象的過程,DirectByteBuffer對象只至關於一個holder,包含一個address,這個是直接內存的指針。this

  • 調用native方法申請內存
  • 初始化cleaner
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer(int cap) {                   // package-private
    // 省略中間代碼...
    // 建立一個cleaner,最後會調用Deallocator.run來釋放內存
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

Cleaner這個類繼承自PhantomReference,也就是所謂的虛引用,這種類型引用的特色是:spa

  • 使用get方法不能獲取到對象
  • 只要引用的對象除了PhantomReference以外沒有其餘引用了,JVM隨時能夠將PhantomReference引用的對象回收。

JVM在回前會將將要被回收的對象放在一個隊列中,因爲Cleaner繼承自PhantomReference,隊列的實現是使用cleaner的.net

private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

這個隊列在PhantomReference的父類Reference中使用到了,Reference這個類在初始化的時候會啓動一個線程來調用cleaner.clean方法,在Reference的靜態代碼塊中啓動線程線程

// java.lang.ref.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);
    // 啓動ReferenceHandler線程
    handler.start();
    // 省略中間代碼...
}

該線程的主要做用就是調用tryHandlePending指針

// java.lang.ref.Reference#tryHandlePending
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) {
            // 調用clean方法
            c.clean();
            return true;
        }

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

System.gc不能回收堆外內存,可是會回收已經沒有使用了DirectByteBuffer對象,該對象被回收的時候會將cleaner對象放入隊列中,在Reference的線程中調用clean方法來回收堆外內存 。cleaner.run執行的是傳入參數的thunk.run方法,這裏thunk是Deallocator,因此最後執行的Deallocator.run方法netty

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

因此最後經過unsafe.freeMemory釋放了申請到的內存。code

總結一下,在申請內存的時候調用的是java.nio.ByteBuffer#allocateDirect對象

會new DirectByteBuffer,經過Cleaner.create建立Cleaner,同時傳入Deallocator做爲Runnable參數,在Cleaner.clean的時候會調用該Deallocator.run來處理

Cleaner繼承自PhantomReference,包含一個ReferenceQueue,在DirectByteBuffer再也不使用的時候,該對象是處於Java堆的,除了該PhantomReference引用了DirectByteBuffer外,沒有其餘引用的時候,jvm會把cleaner對象放入ReferenceQueue隊列中。

PhantomReference繼承了Reference,Reference會啓動一個線程(java.lang.ref.Reference.ReferenceHandler#run)去調用隊列中的cleaner.clean方法。

Netty內存釋放

Netty使用的直接內存的釋放方式和JDK的釋放方式略有不一樣。Netty開始釋放內存的時候是調用free方法的時候

io.netty.buffer.PoolArena#free
io.netty.buffer.PoolArena.DirectArena#destroyChunk

最終釋放內存的方法有兩種

  1. 利用反射獲取unsafe,調用Unsafe#freeMemory
  2. 利用反射獲取DirectByteBuffer#cleaner,經過反射調用cleaner.clean方法

兩種不一樣的方式依賴的條件不一樣,使用場景也不一樣

使用反射調用cleaner.clean

要知足如下條件之一的時候使用這種方式

  1. 沒有可以使用的直接內存
  2. 不能獲取unsafe
  3. directBuffer沒有傳入long、int的構造方法

使用unsafe

不能使用上面這種方式的都使用unsafe

相關文章
相關標籤/搜索