• 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;
        }
  • 相关阅读:
    初识redis
    支付宝退款操作
    《地质灾害防治这一年2013》读书摘要
    地质灾害防治工作的经验和体会
    关于地质灾害防治的一些认识
    应急信息报送和值班工作的培训学习
    《地质灾害防治这一年2012》读书摘要
    关于开源GIS和商业GIS的讨论
    B树索引学习
    cordova 开发 ios app 简要流程
  • 原文地址:https://www.cnblogs.com/xiaojiesir/p/15449937.html
Copyright © 2020-2023  润新知