3983金沙官网堆外内部存款和储蓄器,堆外内部存

作者:国际新闻

原标题:JDK 源码阅读 : DirectByteBuffer

堆外内存, JDK 1.4 nio引进了ByteBuffer.allocateDirect()分配堆外内存

最近在查一个堆外内存泄露的问题,顺便学习了下MaxDirectMemorySize使用。
总所周知-XX:MaxDirectMemorySize可以设置java堆外内存的峰值,但是具体是在哪里限制的呢,来跟踪下创建DirectByteBuffer的过程。

最近在查一个堆外内存泄露的问题,通过-XX:MaxDirectMemorySize仍然限制不住堆外内存的上涨,一直到机器物理内存爆满,被oom killer。

堆外内存

堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。

来源:木杉的博客 ,

style="font-size: 16px;">imushan.com/2018/08/29/java/language/JDK源码阅读-DirectByteBuffer/

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

  • DirectByteBuffer
    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);//用Bits类保存总分配内存(按页分配)的大小和实际内存的大小
    long base = 0;
    try {//在堆外内存的基地址,指定内存大小
    base = unsafe.allocateMemory(size);//unsafe.cpp中调用os::malloc分配内存
    } 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;
    }

  • Deallocator
    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);//调用OS的方法释放地址,os::free
    address = 0;
    Bits.unreserveMemory(size, capacity);//统计堆外内存大小
    }
    }

  • Cleaner
    public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();//static数据
    private static Cleaner first = null;//static数据
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;//Deallocator对象,每个cleaner对象都保留了一个Deallocator对象,它里面有address基地址等
    private static synchronized Cleaner add(Cleaner var0) {
    if(first != null) {
    var0.next = first;
    first.prev = var0;
    }
    first = var0;
    return var0;
    }
    private Cleaner(Object var1, Runnable var2) {
    super(var1, dummyQueue);//var1 传的是DirectByteBuffer对象
    this.thunk = var2;//Deallocator对象
    }
    public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null?null:add(new Cleaner(var0, var1));//var0传的是DirectByteBuffer对象
    }

  • Bits
    // -- Direct memory management --
    // A user-settable upper limit on the maximum amount of allocatable direct buffer memory.
    // This value may be changed during VM initialization if it is launched with "-XX:MaxDirectMemorySize=<size>".
    private static volatile long maxMemory = VM.maxDirectMemory();
    private static volatile long reservedMemory;
    private static volatile long totalCapacity;
    private static volatile long count;
    private static boolean memoryLimitSet = false;
    // These methods should be called whenever direct memory is allocated or
    // freed. They allow the user to control the amount of direct memory
    // which a process may access. All sizes are specified in bytes.
    static void reserveMemory(long size, int cap) {
    synchronized (Bits.class) {
    if (!memoryLimitSet && VM.isBooted()) {
    maxMemory = VM.maxDirectMemory();// 67108864L == 64MB
    memoryLimitSet = true;
    }
    // -XX:MaxDirectMemorySize limits the total capacity rather than the
    // actual memory usage, which will differ when buffers are page aligned.
    if (cap <= maxMemory - totalCapacity) {
    reservedMemory = size;
    totalCapacity = cap;
    count ;
    return;
    }
    }
    System.gc();//内存不够了, try gc
    try {
    Thread.sleep(100);
    } catch (InterruptedException x) {
    // Restore interrupt status
    Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
    if (totalCapacity cap > maxMemory)
    throw new OutOfMemoryError("Direct buffer memory");
    reservedMemory = size;
    totalCapacity = cap;
    count ;
    }
    }
    static synchronized void unreserveMemory(long size, int cap) {
    if (reservedMemory > 0) {
    reservedMemory -= size;
    totalCapacity -= cap;
    count--;
    assert (reservedMemory > -1);
    }
    }

  • DirectByteBuffer被回收

    DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象何时被回收的,
    它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,
    那将会把这个引用(Cleaner)放到java.lang.ref.Reference.pending队列里,
    在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,
    而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,
    在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块

  • JDK里面的ReferenceHandler实现
    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();//直接调用clean方法清理
    continue;
    }
    ReferenceQueue q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    }
    }
    }

  • 简单流程梳理

    • 堆外内存的申请
      • ByteBuffer.allocateDirect()
      • unsafe.allocateMemory()
      • os::malloc()
    • 堆外内存的释放
      • cleaner.clean()
        • 把自身从Clener链表删除,从而在下次GC时能够被回收
        • 释放堆外内存
      • unsafe.freeMemory()
      • os::free()
  • 对象的引用关系

    • 3983金沙官网 1

      初始化时

    • 3983金沙官网 2

      如果该DirectByteBuffer对象在一次GC中被回收了

  • 不过很多线上环境的JVM参数有-XX: DisableExplicitGC,导致了System.gc()等于一个空函数,根本不会触发FGC,这一点在使用Netty框架时需要注意是否会出问题

  • 关于直接内存默认值是否为64MB?

    • java.lang.System
      private static void initializeSystemClass() {//Initialize the system class. Called after thread initialization.
      ...
      sun.misc.VM.saveAndRemoveProperties(props);
      ...
      }
    • saveAndRemoveProperties(){
      // Set the maximum amount of direct memory. This value is controlled
      // by the vm option -XX:MaxDirectMemorySize=<size>.
      // The maximum amount of allocatable direct buffer memory (in bytes)
      // from the system property sun.nio.MaxDirectMemorySize set by the VM.
      // The system property will be removed.
      String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
      if (s != null) {
      if (s.equals("-1")) {
      // -XX:MaxDirectMemorySize not given, take default
      directMemory = Runtime.getRuntime().maxMemory();
      } else {
      long l = Long.parseLong(s);
      if (l > -1)
      directMemory = l;
      }
      }}
    • 如果我们通过-Dsun.nio.MaxDirectMemorySize指定了这个属性,只要它不等于-1,那效果和加了-XX:MaxDirectMemorySize一样的,如果两个参数都没指定,那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法
    • Universe::heap()->max_capacity();
    • 其中在我们使用CMS GC的情况下的实现如下,其实是新生代的最大值-一个survivor的大小 老生代的最大值,也就是我们设置的-Xmx的值里除去一个survivor的大小就是默认的堆外内存的大小了
  • 如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理

  • 可见如果pending为空的时候,会通过lock.wait()一直等在那里,其中唤醒的动作是在jvm里做的,当gc完成之后会调用如下的方法VM_GC_Operation::doit_epilogue(),在方法末尾会调用lock的notify操作,至于pending队列什么时候将引用放进去的,其实是在gc的引用处理逻辑中放进去的,针对引用的处理后面可以专门写篇文章来介绍

  • 对于System.gc的实现,它会对新生代和老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存,我们dump内存发现DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为『冰山对象』,我们做ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题,如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc)。

  • 我们通信过程中如果数据是在Heap里的,最终也还是会copy一份到堆外,然后再进行发送,所以为什么不直接使用堆外内存呢

  • gc机制与堆外内存的关系也说了,如果一直触发不了cms gc或者full gc,那么后果可能很严重

  • References

    • 笨神-JVM源码分析之堆外内存完全解读
    • 占小狼-堆外内存的回收机制分析
    • 从0到1起步-跟我进入堆外内存的奇妙世界

找到DirectByteBuffer的构造函数

上一篇关于MaxDirectMemorySize的设置中讲过MaxDirectMemorySize限制了DirectByteBuffer的内存申请,但是它只是通过java.nio.Bits类中reservedMemory,totalCapacity的值去做计算,真正申请堆外内存空间的是sun.misc.Unsafe类。也就是说,用户完全可以通过其他方式使内存泄露。

在讲解DirectByteBuffer之前,需要先简单了解两个知识点

在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计。但是他是一个抽象类,真正的实现分为两类:HeapByteBuffer与DirectByteBuffer。HeapByteBuffer是堆内ByteBuffer,使用byte[]存储数据,是对数组的封装,比较简单。DirectByteBuffer是堆外ByteBuffer,直接使用堆外内存空间存储数据,是NIO高性能的核心设计之一。本文来分析一下DirectByteBuffer的实现。

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

这里就遇到了一个由于句柄泄露导致内存泄露的问题。

java引用类型,因为DirectByteBuffer是通过虚引用(Phantom Reference)来实现堆外内存的释放的。

PhantomReference 是所有“弱引用”中最弱的引用类型。不同于软引用和弱引用,虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。
那虚引用到底有什么作用?其实虚引用主要被用来 跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否 即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的作用。
关于java引用类型的实现和原理可以阅读之前的文章Reference 、ReferenceQueue 详解 和Java 引用类型简述

style="font-size: 16px;">

主要代码:

3983金沙官网 3

关于linux的内核态和用户态

3983金沙官网 4

  • 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。比如socket I/0操作或者文件的读写操作等
  • 用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。
  • 系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口。

3983金沙官网 5

因此我们可以得知当我们通过JNI调用的native方法实际上就是从用户态切换到了内核态的一种方式。并且通过该系统调用使用操作系统所提供的功能。

Q:为什么需要用户进程(位于用户态中)要通过系统调用(Java中即使JNI)来调用内核态中的资源,或者说调用操作系统的服务了?
A:intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为内核态。Ring3状态不能访问Ring0的地址空间,包括代码和数据。因此用户态是没有权限去操作内核态的资源的,它只能通过系统调用外完成用户态到内核态的切换,然后在完成相关操作后再有内核态切换回用户态。

如何使用DirectByteBuffer

  • Bits.reserveMemory(size, cap);检测是否足够分配空间,并增加内部总量计数器
  • unsafe.allocateMemory(size); 通过Unsafe类分配内存地址
  • cleaner = Cleaner.create(this, new Deallocator(base, size, cap));创建清理类,用于内存回收
    java.nio.Bits

88877.png

DirectByteBuffer ———— 直接缓冲

DirectByteBuffer是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁。

3983金沙官网 6

DirectByteBuffer该类本身还是位于Java内存模型的堆中。堆内内存是JVM可以直接管控、操纵。
而DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在DirectByteBuffer一定会存在某种方式来操纵堆外内存。
在DirectByteBuffer的父类Buffer中有个address属性:

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

address只会被直接缓存给使用到。之所以将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率。
address表示分配的堆外内存的地址。

3983金沙官网 7

unsafe.allocateMemory(size);分配完堆外内存后就会返回分配的堆外内存基地址,并将这个地址赋值给了address属性。这样我们后面通过JNI对这个堆外内存操作时都是通过这个address来实现的了。

在前面我们说过,在linux中内核态的权限是最高的,那么在内核态的场景下,操作系统是可以访问任何一个内存区域的,所以操作系统是可以访问到Java堆的这个内存区域的。
Q:那为什么操作系统不直接访问Java堆内的内存区域了?
A:这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地质,那么该内存地址指向的是Java堆内内存的话,那么如果在操作系统正在访问这个内存地址的时候,Java在这个时候进行了GC操作,而GC操作会涉及到数据的移动操作[GC经常会进行先标志在压缩的操作。即,将可回收的空间做标志,然后清空标志位置的内存,然后会进行一个压缩,压缩就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。

Q:如上面所说,JNI调用的内存是不能进行GC操作的,那该如何解决了?
A:①堆内内存与堆外内存之间数据拷贝的方式(并且在将堆内内存拷贝到堆外内存的过程JVM会保证不会进行GC操作):比如我们要完成一个从文件中读数据到堆内内存的操作,即FileChannelImpl.read(HeapByteBuffer)。这里实际上File I/O会将数据读到堆外内存中,然后堆外内存再讲数据拷贝到堆内内存,这样我们就读到了文件中的内存。

3983金沙官网 8

    static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1.isReadOnly()) {
            throw new IllegalArgumentException("Read-only buffer");
        } else if (var1 instanceof DirectBuffer) {
            return readIntoNativeBuffer(var0, var1, var2, var4);
        } else {
            // 分配临时的堆外内存
            ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());

            int var7;
            try {
                // File I/O 操作会将数据读入到堆外内存中
                int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
                var5.flip();
                if (var6 > 0) {
                    // 将堆外内存的数据拷贝到堆外内存中
                    var1.put(var5);
                }

                var7 = var6;
            } finally {
                // 里面会调用DirectBuffer.cleaner().clean()来释放临时的堆外内存
                Util.offerFirstTemporaryDirectBuffer(var5);
            }

            return var7;
        }
    }

而写操作则反之,我们会将堆内内存的数据线写到对堆外内存中,然后操作系统会将堆外内存的数据写入到文件中。
② 直接使用堆外内存,如DirectByteBuffer:这种方式是直接在堆外分配一个内存(即,native memory)来存储数据,程序通过JNI直接将数据读/写到堆外内存中。因为数据直接写入到了堆外内存中,所以这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操作了。这样在进行I/O操作时,只需要将这个堆外内存地址传给JNI的I/O的函数就好了。

如果需要实例化一个DirectByteBuffer,可以使用java.nio.ByteBuffer#allocateDirect这个方法:

通过mat查看dump文件,
由于句柄泄露导致生成大量的EPollArrayWrapper对象。
这个对象占了多少内存呢,来看下构造函数:

DirectByteBuffer堆外内存的创建和回收的源码解读

public static ByteBuffer allocateDirect(int capacity) {

return new DirectByteBuffer(capacity);

}

// These methods should be called whenever direct memory is allocated or  
    // freed.  They allow the user to control the amount of direct memory  
    // which a process may access.  All sizes are specified in bytes.  
    static void reserveMemory(long size, int cap) {  
        //因为内存分配是全局的,所以必须加锁
        synchronized (Bits.class) {  
            if (!memoryLimitSet && VM.isBooted()) {  
                maxMemory = VM.maxDirectMemory();  //最大堆外内存设置
                memoryLimitSet = true;  
            }  
            // -XX:MaxDirectMemorySize limits the total capacity rather than the  
            // actual memory usage, which will differ when buffers are page  
            // aligned.  
            //如果剩余空间足够,增加总量计数器直接返回
            if (cap <= maxMemory - totalCapacity) { 
                reservedMemory  = size;  
                totalCapacity  = cap;  
                count  ;  
                return;  
            }  
        }  

        //如果剩余空间不足,那么先执行一次GC
        System.gc();  
        try {  
            Thread.sleep(100);//其实jvm也用了很low的sleep下,等待GC完成
        } catch (InterruptedException x) {  
            // Restore interrupt status  
            Thread.currentThread().interrupt();  
        }  
        //这里同样,在计算的时候必须是同步的
        synchronized (Bits.class) {  
            //如果内存空间还是不够,则抛出异常
            if (totalCapacity   cap > maxMemory)
                throw new OutOfMemoryError("Direct buffer memory");  
            reservedMemory  = size;  
            totalCapacity  = cap;  
            count  ;  
        }  

    } 
...省略大量代码

static final int SIZE_EPOLLEVENT  = sizeofEPollEvent();
static final int NUM_EPOLLEVENTS  = Math.min(fdLimit(), 8192);

private static native int sizeofEPollEvent();
private static native int fdLimit();

EPollArrayWrapper() {
        // creates the epoll file descriptor
        epfd = epollCreate();

        // the epoll_event array passed to epoll_wait
        int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
        pollArray = new AllocatedNativeObject(allocationSize, true);
        pollArrayAddress = pollArray.address();

        for (int i=0; i<NUM_EPOLLEVENTS; i  ) {
            putEventOps(i, 0);
            putData(i, 0L);
        }

        // create idle set
        idleSet = new HashSet<SelChImpl>();
}
...
class NativeObject {
  ...
  protected NativeObject(int var1, boolean var2) {
        if(!var2) {
            this.allocationAddress = unsafe.allocateMemory((long)var1);
            this.address = this.allocationAddress;
        } else {
            int var3 = pageSize();
            long var4 = unsafe.allocateMemory((long)(var1   var3));
            this.allocationAddress = var4;
            this.address = var4   (long)var3 - (var4 & (long)(var3 - 1));
        }
  }
  ...
}

堆外内存分配

    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 {
            // 通过unsafe.allocateMemory分配堆外内存,并返回堆外内存的基地址
            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对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

DirectByteBuffer实例化流程

在GC的时候totalCapacity会被释放,看下具体实现。
在DirectByteBuffer中的内部类Deallocator:

EPollArrayWrapper在新建的时候创建了一个NativeObject,
而NativeObject通过unsafe.allocateMemory申请了堆外内存。
其中allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT

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

        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
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // trigger VM's Reference processing
        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;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        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();
            }
        }
    }

该方法用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。

其中,如果系统中内存( 即,堆外内存 )不够的话:

        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
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

jlra.tryHandlePendingReference()会触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。
因为在Reference的静态代码块中定义了:

        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });

如果在进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则

        // trigger VM's Reference processing
        System.gc();

System.gc()会触发一个full gc,当然前提是你没有显示的设置-XX: DisableExplicitGC来禁用显式GC。并且你需要知道,调用System.gc()并不能够保证full gc马上就能被执行。
所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError("Direct buffer memory”)异常。

3983金沙官网 9

注意,这里之所以用使用full gc的很重要的一个原因是:System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存.
DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为冰山对象.
我们做ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题。( 并且堆外内存多用于生命期中等或较长的对象 )
如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc – JVM参数DisableExplicitGC)。

总的来说,Bits.reserveMemory(size, cap)方法在可用堆外内存不足以分配给当前要创建的堆外内存大小时,会实现以下的步骤来尝试完成本次堆外内存的创建:
① 触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。
② 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()。System.gc()会触发一个full gc,但你需要知道,调用System.gc()并不能够保证full gc马上就能被执行。所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。
注意,如果你设置了-XX: DisableExplicitGC,将会禁用显示GC,这会使System.gc()调用无效。
③ 如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError("Direct buffer memory”)异常。

那么可用堆外内存到底是多少了?,即默认堆外存内存有多大:
① 如果我们没有通过-XX:MaxDirectMemorySize来指定最大的堆外内存。则

本文由3983金沙官网发布,转载请注明来源

关键词: