• 【转载】 DirectByteBuffer内存释放


    http://www.tianshouzhi.com/api/tutorials/netty/331

    我们已经知道,在网络编程中,为了避免频繁的在用户空间与内核空间拷贝数据,通常会直接从内核空间中申请内存,存放数据,在Java中,把内核空间的内存称之为直接内存,nio包中的ByteBufferallocateDirect方法,就是帮助我们申请直接内存的,代码如下所示:

    1. public static ByteBuffer allocateDirect(int capacity) {
    2.     return new DirectByteBuffer(capacity);
    3. }

    在上述代码片段中,返回的是一个DirectByteBuffer对象,其是ByteBuffer的子类,对于直接内存的分配,就是在这个类中实现的。

    有经验的读者可能知道,在java中,直接内存的申请与释放是通过Unsafe类的allocateMemory方法和freeMemory方法来实现的,且对于直接内存的释放,必须手工调用freeMemory方法,因为JVM只能帮我们管理堆内存,直接内存不在其管理范围之内。

    DirectByteBuffer帮我们简化了直接内存的使用,我们不需要直接操作Unsafe类来进行直接内存的申请与释放,那么其是如何实现的呢?

    直接内存的申请:

    在DirectByteBuffer实例通过构造方法创建的时候,会通过Unsafe类的allocateMemory方法 帮我们申请直接内存资源。

    直接内存的释放:

    DirectByteBuffer本身是一个Java对象,其是位于堆内存中的,JDK的GC机制可以自动帮我们回收,但是其申请的直接内存,不再GC范围之内,无法自动回收。好在JDK提供了一种机制,可以为堆内存对象注册一个钩子函数(其实就是实现Runnable接口的子类),当堆内存对象被GC回收的时候,会回调run方法,我们可以在这个方法中执行释放DirectByteBuffer引用的直接内存,即在run方法中调用Unsafe 的freeMemory 方法。注册是通过sun.misc.Cleaner类来实现的。

    下面通过源码进行分析:

    1. class DirectByteBuffer extends MappedByteBuffer  implements DirectBuffer
    2. {
    3.     ....
    4.     //构造方法
    5.     DirectByteBuffer(int cap) {                   // package-private
    6.     
    7.         super(-1, 0, cap, cap);
    8.         boolean pa = VM.isDirectMemoryPageAligned();
    9.         int ps = Bits.pageSize();
    10.         long size = Math.max(1L, (long)cap + (pa ? ps : 0));//对申请的直接内存大小,进行重新计算
    11.         Bits.reserveMemory(size, cap);
    12.     
    13.         long base = 0;
    14.         try {
    15.             base = unsafe.allocateMemory(size); //分配直接内存,base表示的是直接内存的开始地址
    16.         } catch (OutOfMemoryError x) {
    17.             Bits.unreserveMemory(size, cap);
    18.             throw x;
    19.         }
    20.         unsafe.setMemory(base, size, (byte) 0);
    21.         if (pa && (base % ps != 0)) {
    22.             // Round up to page boundary
    23.             address = base + ps - (base & (ps - 1));
    24.         } else {
    25.             address = base;
    26.         }
    27.         cleaner = Cleaner.create(this, new Deallocator(base, size, cap));//注册钩子函数,释放直接内存
    28.         att = null;
    29.     
    30.     }
    31.       ....
    32. }

    可以看到构造方法中的确是用了unsafe.allocateMemory方法帮我们分配了直接内存,另外,在构造方法的最后,通过 Cleaner.create方法注册了一个钩子函数,用于清除直接内存的引用。

    Cleaner.create方法声明如下所示:

    1. public static Cleaner create(Object heapObj, Runnable task)

    其中第一个参数是一个堆内存对象,第二个参数是一个Runnable任务,表示这个堆内存对象被回收的时候,需要执行的回调方法。我们可以看到在DirectByteBuffer的最后一行中,传入的这两个参数分别是this,和一个Deallocator(实现了Runnable接口),其中this表示就是当前DirectByteBuffer实例,也就是当前DirectByteBuffer被回收的时候,回调Deallocator的run方法

    Deallocator就是用于清除DirectByteBuffer引用的直接内存,代码如下所示:

    1. private static class Deallocator
    2.     implements Runnable
    3. {
    4.  
    5.     private static Unsafe unsafe = Unsafe.getUnsafe();
    6.  
    7.     private long address;
    8.     private long size;
    9.     private int capacity;
    10.  
    11.     private Deallocator(long address, long size, int capacity) {
    12.         assert (address != 0);
    13.         this.address = address;
    14.         this.size = size;
    15.         this.capacity = capacity;
    16.     }
    17.  
    18.     public void run() {
    19.         if (address == 0) {
    20.             // Paranoia
    21.             return;
    22.         }
    23.         unsafe.freeMemory(address);//清除直接内存
    24.         address = 0;
    25.         Bits.unreserveMemory(size, capacity);
    26.     }
    27.  
    28. }

    可以看到run方法中调用了unsafe.freeMemory方法释放了直接内存的引用。

    关于System.gc对直接内存释放的影响

    细心的读者,可能注意到了,在DirectByteBuffer实例创建的时候,分配内存之前调用了Bits.reserveMemory方法,如果分配失败调用了Bits.unreserveMemory,同时在Deallocator释放完直接内存的时候,也调用了Bits.unreserveMemory方法。

    这两个方法,主要是记录jdk已经使用的直接内存的数量,当分配直接内存时,需要进行增加,当释放时,需要减少,源码如下:

    1. static void reserveMemory(long size, int cap) {
    2.     //如果直接有足够多的直接内存可以用,直接增加直接内存引用的计数
    3.     synchronized (Bits.class) {
    4.         if (!memoryLimitSet && VM.isBooted()) {
    5.             maxMemory = VM.maxDirectMemory();
    6.             memoryLimitSet = true;
    7.         }
    8.         // -XX:MaxDirectMemorySize limits the total capacity rather than the
    9.         // actual memory usage, which will differ when buffers are page
    10.         // aligned.
    11.         if (cap <= maxMemory - totalCapacity) {//维护已经使用的直接内存的数量
    12.             reservedMemory += size;
    13.             totalCapacity += cap;
    14.             count++;
    15.             return;
    16.         }
    17.     }
    18.    //如果没有有足够多的直接内存可以用,先进行垃圾回收
    19.     System.gc();
    20.     try {
    21.         Thread.sleep(100);//休眠100秒,等待垃圾回收完成
    22.     } catch (InterruptedException x) {
    23.         // Restore interrupt status
    24.         Thread.currentThread().interrupt();
    25.     }
    26.     synchronized (Bits.class) {//休眠100毫秒后,增加直接内存引用的计数
    27.         if (totalCapacity + cap > maxMemory)
    28.             throw new OutOfMemoryError("Direct buffer memory");
    29.         reservedMemory += size;
    30.         totalCapacity += cap;
    31.         count++;
    32.     }
    33.  
    34. }
    35. //释放内存时,减少引用直接内存的计数
    36. static synchronized void unreserveMemory(long size, int cap) {
    37.     if (reservedMemory > 0) {
    38.         reservedMemory -= size;
    39.         totalCapacity -= cap;
    40.         count--;
    41.         assert (reservedMemory > -1);
    42.     }
    43. }

    通过上面代码的分析,我们事实上可以认为Bits类是直接内存的分配担保,当有足够的直接内存可以用时,增加直接内存应用计数,否则,调用System.gc,进行垃圾回收,需要注意的是,System.gc只会回收堆内存中的对象,但是我们前面已经讲过,DirectByteBuffer对象被回收时,那么其引用的直接内存也会被回收,试想现在刚好有其他的DirectByteBuffer可以被回收,那么其被回收的直接内存就可以用于本次DirectByteBuffer直接的内存的分配。

    因此我们经常看到,有一些文章讲解在使用Nio的时候,不要禁用System.gc,也就是启动JVM的时候,不要传入-XX:+DisableExplicitGC参数,因为这样可能会造成直接内存溢出。道理很明显,因为直接内存的释放与获取比堆内存更加耗时,每次创建DirectByteBuffer实例分配直接内存的时候,都调用System.gc,可以让已经使用完的DirectByteBuffer得到及时的回收。

    虽然System.gc只是建议JVM去垃圾回收,可能JVM并不会立即回收,但是频繁的建议,JVM总不会视而不见。

    不过,这并不是绝对的,因为System.gc导致的是FullGC,可能会暂停用户线程,也就是JVM不能继续响应用户的请求,对于一些要求延时比较短的应用,是不希望JVM频繁的进行FullGC的。

    所以笔者的建议是:禁用System.gc,调大最大可以使用的直接内存。如:

    -XX:+DisableExplicitGC -XX:MaxDirectMemorySize=256M
     
  • 相关阅读:
    RabbitMQ和Kafka的区别
    如何在 Vim 中复制,剪切,粘贴
    python中的堆支持自定义的比较函数
    LLDP协议(笔记草稿)
    You may have an infinite update loop in a component render function,vue模板报错
    修改element ui样式,el-dialog__header样式,并且不影响全局
    js,小数字符串去除右边零显示
    egg-sequelize创建表
    Cyclic dependency found. Users is dependent of itself,mysql表循环依赖问题
    eggjs sequelize操作多个数据,表名不加s,不默认加创建和修改时间字段
  • 原文地址:https://www.cnblogs.com/exmyth/p/14188683.html
Copyright © 2020-2023  润新知