java對外內存簡介

\color{#39c154}{堆外內存簡介}

DirectByteBuffer 這個類是 JDK 提供使用堆外內存的一種途徑,當然常見的業務開發一般不會接觸到,即使涉及到也可能是框架(如 Netty、RPC 等)使用的,對框架使用者來說也是透明的。

\color{#39c154}{堆外內存優勢}

堆外內存優勢在 IO 操作上,對於網絡 IO,使用 Socket 發送數據時,能夠節省堆內存到堆外內存的數據拷貝,所以性能更高。看過 Netty 源碼的同學應該瞭解,Netty 使用堆外內存池來實現零拷貝技術。對於磁盤 IO 時,也可以使用內存映射,來提升性能。另外,更重要的幾乎不用考慮堆內存煩人的 GC 問題。

\color{#39c154}{堆外內存創建}

我們直接來看代碼,首先向 Bits 類申請額度,Bits 類內部維護着當前已經使用的堆外內存值,會 check 當前申請的大小與已經使用的內存大小是否超過總的堆外內存大小(默認大小與堆內存差不多,其實是有細微區別的,拿 CMS GC 來舉例,它的大小是新生代的最大值 - 一個 survivor 的大小 + 老生代的最大值),可以使用 -XX:MaxDirectMemorySize 參數指定堆外內存最大大小。

       //
        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;
        }

如果 check 不通過,會主動執行 System.gc(),然後 sleep 100 毫秒,再進行 check,如果內存還是不足,就拋出 OOM Error。

如果 check 通過,就會調用 unsafe.allocateMemory 真正分配內存,返回內存地址,然後再將內存清 0。題外話,這個 unsafe 命名看着是不是很嚇人,這個 unsafe 不是說不安全,而是 JDK 內部使用的類,不推薦外部使用,所以叫 unsafe,Netty 源碼內部也有類似命名。

由於申請內存前可能會調用 System.gc(),所以謹慎設置 -XX:+DisableExplicitGC 這個選項,這個參數作用是禁止代碼中顯示觸發的 Full GC。

\color{#39c154}{堆外內存回收}

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

看到這段代碼從成員的命名上就應該知道,是用來回收堆外內存的。確實,但是它是如何工作的呢?接下來我們看看 Cleaner 類。

    public class Cleaner extends PhantomReference {
        private static final ReferenceQueue dummyQueue = new ReferenceQueue();
        private static Cleaner first = null;
        private Cleaner next = null;
        private Cleaner prev = null;
        private final Runnable thunk;
        
        private static synchronized Cleaner add(Cleaner var0) {
           ...
        }
        
        private static synchronized boolean remove(Cleaner var0) {
            ...
        }

        private Cleaner(Object var1, Runnable var2) {
            super(var1, dummyQueue);
            this.thunk = var2;
        }

        public static Cleaner create(Object var0, Runnable var1) {
            return var1 == null?null:add(new Cleaner(var0, var1));
        }

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

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

            }
        }
    }

Cleaner 類,內部維護了一個 Cleaner 對象的鏈表,通過 create(Object, Runnable) 方法創建 cleaner 對象,調用自身的 add 方法,將其加入到鏈表中。更重要的是提供了 clean 方法,clean 方法首先將對象自身從鏈表中刪除,保證只調用一次,然後執行 this.thunk 的 run 方法,thunk 就是由創建時傳入的 Runnable 參數,也就是說 clean 只負責觸發 Runnable 的 run 方法,至於 Runnable 做什麼任務它不關心。

那 DirectByteBuffer 傳進來的 Runnable 是什麼呢?

     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);
            }
        }

Deallocator 類的對象就是 DirectByteBuffer 中的 cleaner 傳進來的 Runnable 參數類,我們直接看 run 方法 unsafe.freeMemory 釋放內存,然後更新 Bits 裏已使用的內存數據。

接下來我們關注各個環節是如何串起來的?這裏主要講兩種回收方式:一種是自動回收,一種是手動回收。

\color{#39c154}{如何自動回收?}

Java 是不用用戶去管理內存的,所以 Java 對堆外內存 默認是自動回收的。它是 由 GC 模塊負責的,在 GC 時會掃描 DirectByteBuffer 對象是否有有效引用指向該對象,如沒有,在回收 DirectByteBuffer 對象的同時且會回收其佔用的堆外內存。但是 JVM 如何釋放其佔用的堆外內存呢?如何跟 Cleaner 關聯起來呢?

這得從 Cleaner 繼承了 PhantomReference(虛引用) 說起。說到 Reference,還有 SoftReference、WeakReference、FinalReference 他們作用各不相同,這裏就不展開說了。

簡單介紹 PhantomReference,首先虛引用是不會影響 JVM 去回收其指向的對象,當 GC 某個對象時,如果有此對象上還有虛引用對其引用,會將 PhantomReference 對象插入 ReferenceQueue 隊列。

PhantomReference插入到哪個隊列呢?看 PhantomReference 類代碼,其繼承自 Reference,Reference 對象有個 ReferenceQueue 成員,這個也就是 PhantomReference 對象插入的 ReferenceQueue 隊列,此成員如果不由外部傳入就是 ReferenceQueue.NULL。如果需要通過 queue 拿到 PhantomReference 對象,這個 ReferenceQueue 對象還是必須由外部傳入。

    private static final ReferenceQueue dummyQueue = new ReferenceQueue();
    private Cleaner(Object var1, Runnable var2) {
            super(var1, dummyQueue);
            this.thunk = var2;
    }
    public class PhantomReference<T> extends Reference<T> {

Reference 類內部 static 靜態塊會啓動 ReferenceHandler 線程,線程優先級很高,這個線程是用來處理 JVM 在 GC 過程中交接過來的 reference。想必經常用 jstack 命令,看線程堆棧的同學應該見到過這個線程。

    public abstract class Reference<T> {

        private T referent;         /* Treated specially by GC */

        ReferenceQueue<? super T> queue;

        Reference next;
        transient private Reference<T> discovered;  /* used by VM */

        static private class Lock { };
        private static Lock lock = new Lock();
        private static Reference pending = null;

        ...

        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();
        }

        public T get() {
            return this.referent;
        }

        public void clear() {
            this.referent = null;
        }

        public boolean isEnqueued() {
                synchronized (this) {
                return (this.queue != ReferenceQueue.NULL) && (this.next != null);
            }
        }

        public boolean enqueue() {
            return this.queue.enqueue(this);
        }

        /* -- Constructors -- */

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

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

    }

我們來看看 ReferenceHandler 是如何處理的?直接看 run 方法,首先是個死循環,一直在那不停的幹活,synchronized 塊內的這段主要是交接 JVM 扔過來的 reference(就是 pending),再往下看,很明顯,調用了 cleaner 的 clean 方法。調完之後直接 continue 結束此次循環,這個 reference 並沒有進入 queue,也就是說 Cleaner 虛引用是不放入 ReferenceQueue。

    /* High-priority thread to enqueue pending References
         */
        private static class ReferenceHandler extends Thread {

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

            public void run() {
                for (;;) {

                    Reference r;
                    synchronized (lock) {
                        if (pending != null) {
                            r = pending;
                            Reference rn = r.next;
                            pending = (rn == r) ? null : rn;
                            r.next = r;
                        } else {
                            try {
                                lock.wait();
                            } catch (InterruptedException x) { }
                            continue;
                        }
                    }

                    // Fast path for cleaners
                    if (r instanceof Cleaner) {
                        ((Cleaner)r).clean();
                        continue;
                    }

                    ReferenceQueue q = r.queue;
                    if (q != ReferenceQueue.NULL) q.enqueue(r);
                }
            }
        }

這塊有點想不通,既然不放入 ReferenceQueue,爲什麼 Cleaner 類還是初始化了這個 ReferenceQueue。

\color{#39c154}{如何手動回收?}

手動回收,就是由開發手動調用 DirectByteBuffer 的 cleaner 的 clean 方法來釋放空間。由於 cleaner 是 private 反問權限,所以自然想到使用反射來實現。

    public static void clean(final ByteBuffer byteBuffer) {
     if (byteBuffer.isDirect()) {
            Field cleanerField = byteBuffer.getClass().getDeclaredField("cleaner");
            cleanerField.setAccessible(true);
            Cleaner cleaner = (Cleaner) cleanerField.get(byteBuffer);
            cleaner.clean();
        }
    }

還有另一種方法,DirectByteBuffer 實現了 DirectBuffer 接口,這個接口有 cleaner 方法可以獲取 cleaner 對象。

    public static void clean(final ByteBuffer byteBuffer) {
        if (byteBuffer.isDirect()) {
            ((DirectBuffer)byteBuffer).cleaner().clean();
        }
    }

Netty 中的堆外內存池就是使用反射來實現手動回收方式進行回收的。
轉自:https://mp.weixin.qq.com/s?__biz=MzU2NjIzNDk5NQ%3D%3D&mid=2247485973

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章