1. 前言

  自从java在1.4版本后有了NIO,direct memory就变得如此的常见。在NIO中,direct memory充当缓冲区,应用的是本机内存而不是堆内存。这种形式缩小了数据在java堆和本机堆之间的复制操作,肯定水平上进步了数据流转的效率。然而direct memory的调配和回收性能不高,不倡议频繁的调配direct memory。

  通常咱们都是通过allocateDirect()调配一块间接内存。这实际上是在堆上新建了一个DirectByteBuffer的java对象,该对象援用了一块间接内存的地址(jvm过程中的虚地址,通过缺页异样调配理论的物理地址)。上面就会介绍通过allocateDirect()调配间接内存的过程以及DirectMemory在hotspot源码中的一些细节。

hotspot源码版本为:openjdk 11.0.14

2. DirectMemory内存调配流程

  通过如下办法能够调配1M的间接内存。

ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);

间接进allocateDirect()办法,能够看到实际上是新建了一个DirectByteBuffer对象。

public static ByteBuffer allocateDirect(int capacity) {        return new DirectByteBuffer(capacity);        }

核心内容在DirectByteBuffer类的构造方法中,源码如下:

DirectByteBuffer(int cap) {                   // package-private        // 应用父类构造方法初始化ByteBuffer指针        super(-1, 0, cap, cap);        // 判断是否设置了 内存对齐(默认false)        boolean pa = VM.isDirectMemoryPageAligned();        int ps = Bits.pageSize();        // 如果不设置内存对齐,size和cap值一样        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;    }

2.1 内存调配前的查看

  外围办法就在Bits.reserveMemory(size, cap);内。源码如下:

    static void reserveMemory(long size, int cap) {        if (!memoryLimitSet && VM.isBooted()) {            // 获取最大间接内存大小            maxMemory = VM.maxDirectMemory();            memoryLimitSet = true;        }        // optimist!        // 这个办法内查看是否存在残余间接内存空间        if (tryReserveMemory(size, cap)) {            // 如果还有空间进行调配,间接返回            return;        }        // 获取Reference对象(这里须要Reference的一些知识点)        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();        // retry while helping enqueue pending Reference objects        // which includes executing pending Cleaner(s) which includes        // Cleaner(s) that free direct buffer memory        // 通过Cleaner尝试开释一部分间接内存        while (jlra.tryHandlePendingReference()) {            // 再次查看残余间接内存容量            if (tryReserveMemory(size, cap)) {                return;            }        }        // trigger VM's Reference processing        // 强制Full GC        // 能够看到,如果间接内存余量查看不通过,就会触发Full GC        System.gc();        // a retry loop with exponential back-off delays        // (this gives VM some time to do it's job)        // 在循环中屡次查看残余间接内存容量        boolean interrupted = false;        try {            long sleepTime = 1;            int sleeps = 0;            while (true) {                if (tryReserveMemory(size, cap)) {                    return;                }                // MAX_SLEEPS为9                if (sleeps >= MAX_SLEEPS) {                    break;                }                if (!jlra.tryHandlePendingReference()) {                    try {                        // 每次循环 睡眠工夫 * 2(单位:ms)                        Thread.sleep(sleepTime);                        sleepTime <<= 1;                        sleeps++;                    } catch (InterruptedException e) {                        interrupted = true;                    }                }            }            // no luck            throw new OutOfMemoryError("Direct buffer memory");        } finally {            if (interrupted) {                // don't swallow interrupts                Thread.currentThread().interrupt();            }        }    }

tryReserveMemory(size, cap)进行间接内存余量查看的源码如下:

private static boolean tryReserveMemory(long size, int cap) {        // -XX:MaxDirectMemorySize limits the total capacity rather than the        // actual memory usage, which will differ when buffers are page        // aligned.        long totalCap;        // totalCapacity记录以后已应用间接内存大小        // 须要调配的大小如果小于  最大间接内存和以后已应用的间接内存的差值,则为true        // 否则,返回false        while (cap <= maxMemory - (totalCap = totalCapacity.get())) {            // 通过CAS将以后已应用间接内存大小 更新为 以后新的值            if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {                // 将已预留间接内存大小 更新 为以后新的值                reservedMemory.addAndGet(size);                // 计数器自增                count.incrementAndGet();                return true;            }        }        return false;    }

2.2 DirectMemory默认最大是多少

  在下面进行残余内存查看的时候,最大间接内存用的是maxMemory变量的值,源码中它的取值如下:

private static volatile long maxMemory = VM.maxDirectMemory();/*** 这里这个directMemory在VM.java中定义了个动态变量,容易误导人,让人* 认为maxDirectMemory的大小就是64M,其实不是的。**/public static long maxDirectMemory() {        return directMemory;    }

其实maxMemory的值并不是就是64M,默认如果不设置,maxMemory的值和最大堆内存大小(-Xmx设置的值)差不多。如果咱们设置了JVM的运行时参数-XX:MaxDirectMemorySize=xxxmaxMemory就是咱们自定义的值。间接看hotspot源码:

在jvm中会将-XX:MaxDirectMemorySize的属性转换成sun.nio.MaxDirectMemorySize属性,如果不设置,则默认设置为-1

在jvm启动的时候会读取下面设置的值,如果是-1,则将directMemory设置为运行时的最大内存(即差不多-Xmx的值)。

maxMemory()也是个native办法,源码如下:

至于为什么说 差不多等于最大堆内存的值,其实是少了一个survivor的空间大小。还是看hotspot源码(maxMemory是如何计算的):
hotspot中对应获取运行时内存的办法是max_capacity(),这个办法的大小计算和垃圾收集器关系密切:

  // The particular choice of collected heap.  static CollectedHeap* heap() { return _collectedHeap; }

能够看到这个版本的hotspot中有8种垃圾收集器

以下以CMS垃圾算法为根底
能够看到CMS垃圾算法下的heap大小其实是:年老代最大内存 ➕ 老年代最大内存

再看年老代的最大内存,其实是减掉了一个survivor的大小,源码如下:

  • 由此可见,DirectMemory默认最大是(Xmx - 1个survivor)的大小;
  • DirectMemory有余会导致Full GC;

2.3 DirectByteBuffer的内存调配

  真正分配内存的办法其实是unsafe.allocateMemory(size),这是个native办法:

hotspot中的实现是在unsafe.cpp中,源码如下:

实际上底层是调用了操作系统的malloc函数进行内存调配,而后返回一个内存地址给java。

2.3.1 总结下direct memory大抵的调配流程:

  1. new一个DirectByteBuffer对象;
  2. DirectByteBuffer对象在执行初始化执行构造方法的时候调用unsafe.allocateMemory(size)分配内存,以内存地址作为返回后果;
  3. jvm调用操作系统malloc函数调配虚拟内存(而后在理论应用中通过缺页异样调配理论的物理内存),返回内存地址给java;
  4. 将内存地址保留至DirectByteBuffer对象的成员变量address中进行援用;

因而DirectByteBuffer自身作为一个java对象存在于jvm堆中,然而持有一个本机内存的内存地址的援用。
DirectByteBuffer在堆中占用的内存很小,然而很可能持有一块很大的本机内存援用。

3. DirectMemory关联的本机内存是如何清理的

  既然间接内存不属于jvm堆内存的一部分,那GC必定是无奈间接治理这块内存区域的,那direct memory是如何进行内存回收的呢?

  后面曾经理解到,间接内存实际上是通过操作系统的malloc函数进行内存调配的,因而内存开释也须要调用操作系统的free函数。java中能够通过unsafe.freeMemory()来调用底层的free函数。

基于这个思路,开释间接内存大只有两种路径:

  1. 手动调用unsafe.freeMemory()进行开释,netty中ByteBuf.release()就是这种形式;
  2. 利用GC机制,在GC的过程中主动调用unsafe.freeMemory()开释不再被援用的间接内存;

明天次要想分享第2种回收形式,也就是如何在GC的过程中开释不再被援用的间接内存。

在开始之前,须要理解一些对于Reference的前置常识。因为通过GC间接回收direct memory的形式,齐全基于PhantomReference虚援用来实现。

这里我间接贴上一位大佬的博客:《【java.lang.ref】PhantomReference & jdk.internal.ref.Cleaner》地址:https://blog.csdn.net/reliveI...

这篇文章很全面的介绍了PhantomReference虚援用的相干常识,并在DirectByteBuffer章节清晰的形容了通过与GC的交互联动,实现direct memory的回收过程。给大佬点个赞。

到这里也就晓得,为啥《2.1 内存调配前的查看》在Bits.reserveMemory(size, cap)办法中要显示调用System.gc()进行Full GC。这是为了尽可能回收不可达的DirectByteBuffer对象,也只有通过GC才会主动触发unsafe.freeMemory()的调用,开释间接内存。