zoukankan      html  css  js  c++  java
  • Netty 堆外内存

    Netty 堆外内存

    在 Java 中对象都是在堆内分配的,通常我们说的JVM 内存也就指的堆内内存,堆内内存完全被JVM 虚拟机所管理,JVM 有自己的垃圾回收算法,对于使用者来说不必关心对象的内存如何回收。

    堆外内存与堆内内存相对应,对于整个机器内存而言,除堆内内存以外部分即为堆外内存。堆外内存不受 JVM 虚拟机管理,直接由操作系统管理。

    堆外内存和堆内内存各有利弊,这里我针对其中重要的几点进行说明。

    1. 堆内内存由 JVM GC 自动回收内存,降低了 Java 用户的使用心智,但是 GC 是需要时间开销成本的,堆外内存由于不受 JVM 管理,所以在一定程度上可以降低 GC 对应用运行时带来的影响。
    2. 堆外内存需要手动释放,这一点跟 C/C++ 很像,稍有不慎就会造成应用程序内存泄漏,当出现内存泄漏问题时排查起来会相对困难。
    3. 当进行网络 I/O 操作、文件读写时,堆内内存都需要转换为堆外内存,然后再与底层设备进行交互,这一点在介绍 writeAndFlush 的工作原理中也有提到,所以直接使用堆外内存可以减少一次内存拷贝。
    4. 堆外内存可以实现进程之间、JVM 多实例之间的数据共享。

    由此可以看出,如果你想实现高效的 I/O 操作、缓存常用的对象、降低 JVM GC 压力,堆外内存是一个非常不错的选择。

    堆外内存的分配

    Java 中堆外内存的分配方式有两种:ByteBuffer#allocateDirect和Unsafe#allocateMemory。

    ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);//分配 10M 堆外内存

    跟进 ByteBuffer.allocateDirect 源码,直接调用的 DirectByteBuffer 构造函数

    public static ByteBuffer allocateDirect(int capacity) {
            return new DirectByteBuffer(capacity);
        }
    DirectByteBuffer(int cap) {                   // package-private
    
            super(-1, 0, cap, cap);
            boolean pa = VM.isDirectMemoryPageAligned();//是否页对齐
            int ps = Bits.pageSize();//页大小,默认是4096字节
            long size = Math.max(1L, (long)cap + (pa ? ps : 0));//对齐的话大小就有页大小+cap,即实际的申请的内存大小大于初始的容量
            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);//设置内存里的初始值为0
            if (pa && (base % ps != 0)) {//地址对齐,且基地址不是页大小的整数倍
                // Round up to page boundary
                address = base + ps - (base & (ps - 1));//将地址改为页大小的整数倍,即是某个页的起始地址 (base & (ps - 1))这个是base对ps取余
            } else {
                address = base;
            }
            cleaner = Cleaner.create(this, new Deallocator(base, size, cap));//创建内存回收器,传的是基地址
            att = null;
    
    
    
        }

    在堆内存放的 DirectByteBuffer 对象并不大,仅仅包含堆外内存的地址、大小等属性,同时还会创建对应的 Cleaner 对象,通过 ByteBuffer 分配的堆外内存不需要手动回收,它可以被 JVM 自动回收。当堆内的 DirectByteBuffer 对象被 GC 回收时,Cleaner 就会用于回收对应的堆外内存。

     真正分配堆外内存的逻辑还是通过 unsafe.allocateMemory(size)

    在 Java 中是不能直接使用 Unsafe 的,但是我们可以通过反射获取 Unsafe 实例,使用方式如下所示。

    private static Unsafe unsafe = null;
    
    static {
    
        try {
    
            Field getUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    
            getUnsafe.setAccessible(true);
    
            unsafe = (Unsafe) getUnsafe.get(null);
    
        } catch (NoSuchFieldException | IllegalAccessException e) {
    
            e.printStackTrace();
    
        }
    
    }

    获得 Unsafe 实例后,我们可以通过 allocateMemory 方法分配堆外内存,allocateMemory 方法返回的是内存地址,使用方法如下所示:

    // 分配 10M 堆外内存
    
    long address = unsafe.allocateMemory(10 * 1024 * 1024);

    与 DirectByteBuffer 不同的是,Unsafe#allocateMemory 所分配的内存必须自己手动释放,否则会造成内存泄漏,这也是 Unsafe 不安全的体现。Unsafe 同样提供了内存释放的操作:

    unsafe.freeMemory(address);

    对于 Java 开发者而言,常用的是 ByteBuffer.allocateDirect 分配方式,我们平时常说的堆外内存泄漏都与该分配方式有关。

    堆外内存的回收

    我们试想这么一种场景,因为 DirectByteBuffer 对象有可能长时间存在于堆内内存,所以它很可能晋升到 JVM 的老年代,所以这时候 DirectByteBuffer 对象的回收需要依赖 Old GC 或者 Full GC 才能触发清理。如果长时间没有 Old GC 或者 Full GC 执行,那么堆外内存即使不再使用,也会一直在占用内存不释放,很容易将机器的物理内存耗尽,这是相当危险的。

    那么在使用 DirectByteBuffer 时我们如何避免物理内存被耗尽呢?因为 JVM 并不知道堆外内存是不是已经不足了,所以我们最好通过 JVM 参数 -XX:MaxDirectMemorySize 指定堆外内存的上限大小,当堆外内存的大小超过该阈值时,就会触发一次 Full GC 进行清理回收,如果在 Full GC 之后还是无法满足堆外内存的分配,那么程序将会抛出 OOM 异常。

    此外在 ByteBuffer.allocateDirect 分配的过程中,如果没有足够的空间分配堆外内存,在 Bits.reserveMemory 方法中也会主动调用 System.gc() 强制执行 Full GC,但是在生产环境一般都是设置了 -XX:+DisableExplicitGC,System.gc() 是不起作用的,所以依赖 System.gc() 并不是一个好办法。

    通过前面堆外内存分配方式的介绍,我们知道 DirectByteBuffer 在初始化时会创建一个 Cleaner 对象,它会负责堆外内存的回收工作,那么 Cleaner 是如何与 GC 关联起来的呢?

    Java 对象有四种引用方式:强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。其中 PhantomReference 是最不常用的一种引用方式,Cleaner 就属于 PhantomReference 的子类,如以下源码所示,PhantomReference 不能被单独使用,需要与引用队列 ReferenceQueue 联合使用。

    public class Cleaner extends PhantomReference<Object> {
        private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();// 引用队列
        private static Cleaner first = null;// 双向链表,避免自身被GC,但是只有头指针
        private Cleaner next = null;
        private Cleaner prev = null;
        private final Runnable thunk;
    
        private static synchronized Cleaner add(Cleaner var0) {
            if (first != null) {
                var0.next = first;
                first.prev = var0;
            }
    
            first = var0;
            return var0;
        }
    
        private static synchronized boolean remove(Cleaner var0) {
            if (var0.next == var0) {
                return false;
            } else {
                if (first == var0) {
                    if (var0.next != null) {
                        first = var0.next;
                    } else {
                        first = var0.prev;
                    }
                }
    
                if (var0.next != null) {
                    var0.next.prev = var0.prev;
                }
    
                if (var0.prev != null) {
                    var0.prev.next = var0.next;
                }
    
                var0.next = var0;
                var0.prev = var0;
                return true;
            }
        }
      //构造函数就是接受一个引用对象和一个任务,其实这个任务就是清除任务Deallocatorprivate 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<Void>() {
                        public Void run() {
                            if (System.err != null) {
                                (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                            }
    
                            System.exit(1);
                            return null;
                        }
                    });
                }
    
            }
        }
    }

    当初始化堆外内存时,内存中的对象引用情况如下图所示,first 是 Cleaner 类中的静态变量,Cleaner 对象在初始化时会加入 Cleaner 链表中。DirectByteBuffer 对象包含堆外内存的地址、大小以及 Cleaner 对象的引用,ReferenceQueue 用于保存需要回收的 Cleaner 对象。

     当发生 GC 时,DirectByteBuffer 对象被回收,内存中的对象引用情况发生了如下变化:

    此时 Cleaner 对象不再有任何引用关系,在下一次 GC 时,该 Cleaner 对象将被添加到 ReferenceQueue 中,并执行 clean() 方法。clean() 方法主要做两件事情:

    1. 将 Cleaner 对象从 Cleaner 链表中移除;
    2. 调用 unsafe.freeMemory 方法清理堆外内存。

     clean() 方法详细过程:

      引用对象被释放,这个虚引用就会被添加到引用队列里,但是在这个之前会先放入一个pending引用链表,然后引用类Reference会有一个守护线程ReferenceHandler会去调用tryHandlePending方法遍历是否存在pendingList,有就会返回,这个是本地方法做的,然后去判断具体引用类型,如果是Cleaner类型,就会执行clean方法,其他的就会放入引用队列,这样我们就可以获取引用队列里的元素,进行后处理了,我们来看看这个守护线程ReferenceHandler:

     /* High-priority thread to enqueue pending References
         */
        private static class ReferenceHandler extends Thread {
    
            private static void ensureClassInitialized(Class<?> clazz) {
                try {
                    Class.forName(clazz.getName(), true, clazz.getClassLoader());
                } catch (ClassNotFoundException e) {
                    throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
                }
            }
    
            static {
                // pre-load and initialize InterruptedException and Cleaner classes
                // so that we don't get into trouble later in the run loop if there's
                // memory shortage while loading/initializing them lazily.
                ensureClassInitialized(InterruptedException.class);
                ensureClassInitialized(Cleaner.class);
            }
    
            ReferenceHandler(ThreadGroup g, String name) {
                super(g, name);
            }
    
            public void run() {
                while (true) {
                    tryHandlePending(true);
                }
            }
        }

    其实就是无限调用外部Reference的tryHandlePending,里面就是真正判断类似和执行相应方法的地方啦,这里能看出来pending应该是个链表,可以循环获取后续的引用:

    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;
        }
  • 相关阅读:
    LeetCode120 Triangle
    LeetCode119 Pascal's Triangle II
    LeetCode118 Pascal's Triangle
    LeetCode115 Distinct Subsequences
    LeetCode114 Flatten Binary Tree to Linked List
    LeetCode113 Path Sum II
    LeetCode112 Path Sum
    LeetCode111 Minimum Depth of Binary Tree
    Windows下搭建PHP开发环境-WEB服务器
    如何发布可用于azure的镜像文件
  • 原文地址:https://www.cnblogs.com/xiaojiesir/p/15449937.html
Copyright © 2011-2022 走看看