zoukankan      html  css  js  c++  java
  • 转 深入理解DirectBuffer

    https://blog.csdn.net/mc90716/article/details/80041757

     

    介绍

        最近在工作中使用到了DirectBuffer来进行临时数据的存放,由于使用的是堆外内存,省去了数据到内核的拷贝,因此效率比用ByteBuffer要高不少。之前看过许多介绍DirectBuffer的文章,在这里从源码的角度上来看一下DirectBuffer的原理。

    用户态和内核态

        Intel的 X86架构下,为了实现外部应用程序与操作系统运行时的隔离,分为了Ring0-Ring3四种级别的运行模式。Linux/Unix只使用了Ring0和Ring3两个级别。Ring0被称为用户态,Ring3被称为内核态。普通的应用程序只能运行在Ring3,并且不能访问Ring0的地址空间。操作系统运行在Ring0,并提供系统调用供用户态的程序使用。如果用户态的程序的某一个操作需要内核态来协助完成(例如读取磁盘上的某一段数据),那么用户态的程序就会通过系统调用来调用内核态的接口,请求操作系统来完成某种操作。

        下图是用户态调用内核态的示意图:

     
    系统调用.jpg

    DirectBuffer的创建

        使用下面一行代码就可以创建一个1024字节的DirectBuffer:

    1.  
       
    2.  
      ByteBuffer.allocateDirect(1024);
    3.  
       
    4.  
       

        该方法调用的是new DirectByteBuffer(int cap)。DirectByteBuffer的构造函数是包级私有的,因此外部是调用不到的。

    下面我们来看一下这行代码背后的逻辑:

    1.  
       
    2.  
      DirectByteBuffer(int cap) { // package-private
    3.  
       
    4.  
      super(-1, 0, cap, cap);
    5.  
       
    6.  
      boolean pa = VM.isDirectMemoryPageAligned(); //是否页对齐
    7.  
       
    8.  
      int ps = Bits.pageSize(); //获取pageSize大小
    9.  
       
    10.  
      long size = Math.max(1L, (long) cap + (pa ? ps : 0)); //如果是页对齐的话,那么就加上一页的大小
    11.  
       
    12.  
      Bits.reserveMemory(size, cap); //对分配的直接内存做一个记录
    13.  
       
    14.  
      long base = 0;
    15.  
       
    16.  
      try {
    17.  
       
    18.  
      base = unsafe.allocateMemory(size); //实际分配内存
    19.  
       
    20.  
      } catch (OutOfMemoryError x) {
    21.  
       
    22.  
      Bits.unreserveMemory(size, cap);
    23.  
       
    24.  
      throw x;
    25.  
       
    26.  
      }
    27.  
       
    28.  
      unsafe.setMemory(base, size, (byte) 0); //初始化内存
    29.  
       
    30.  
      //计算地址
    31.  
       
    32.  
      if (pa && (base % ps != 0)) {
    33.  
       
    34.  
      // Round up to page boundary
    35.  
       
    36.  
      address = base + ps - (base & (ps - 1));
    37.  
       
    38.  
      } else {
    39.  
       
    40.  
      address = base;
    41.  
       
    42.  
      }
    43.  
       
    44.  
      //生成Cleaner
    45.  
       
    46.  
      cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    47.  
       
    48.  
      att = null;
    49.  
       
    50.  
      }
    51.  
       
    52.  
       

        DirectBuffer的构造函数主要做以下三个事情:
    1、根据页对齐和pageSize来确定本次的要分配内存实际大小
    2、实际分配内存,并且记录分配的内存大小
    3、声明一个Cleaner对象用于清理该DirectBuffer内存

    需要注意的是DirectBuffer的创建是比较耗时的,所以在一些高性能的中间件或者应用下一般会做一个对象池,用于重复利用DirectBuffer。

    DirectBuffer的使用

        查看DirectBuffer类的方法声明,对于DirectBuffer的使用主要有两类方法,putXXX和getXXX。

    putXXX方法(以putInt为例):

    1.  
       
    2.  
      public ByteBuffer putInt(int x) {
    3.  
       
    4.  
      putInt(ix(nextPutIndex((1 << 2))), x);
    5.  
       
    6.  
      return this;
    7.  
       
    8.  
      }
    9.  
       
    10.  
      private ByteBuffer putInt(long a, int x) {
    11.  
       
    12.  
      if (unaligned) {
    13.  
       
    14.  
      int y = (x);
    15.  
       
    16.  
      unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));
    17.  
       
    18.  
      } else {
    19.  
       
    20.  
      Bits.putInt(a, x, bigEndian);
    21.  
       
    22.  
      }
    23.  
       
    24.  
      return this;
    25.  
       
    26.  
      }
    27.  
       
    28.  
       

        putInt方法会根据是否是内存对齐分别调用unsafe.putInt或者Bits.putInt来把数据放到直接内存中。Bits.putInt实际上会根据是大端或者是小端来区分如何把数据放到直接内存中,放的方式同样是调用unsage.putInt。

    getXXX方法(以getInt为例):

    1.  
       
    2.  
      public int getInt() {
    3.  
       
    4.  
      return getInt(ix(nextGetIndex((1 << 2))));
    5.  
       
    6.  
      }
    7.  
       
    8.  
      private int getInt(long a) {
    9.  
       
    10.  
      if (unaligned) {
    11.  
       
    12.  
      int x = unsafe.getInt(a);
    13.  
       
    14.  
      return (nativeByteOrder ? x : Bits.swap(x));
    15.  
       
    16.  
      }
    17.  
       
    18.  
      return Bits.getInt(a, bigEndian);
    19.  
       
    20.  
      }
    21.  
       
    22.  
       

        首先判断是否是页对齐,如果不是页对齐,那么直接通过unsafe.getInt来获取数据;如果是页对齐,那么通过Bits.getInt方法来获取数据。Bits.getInt同样是根据大端还是小端,调用unsafe.getInt来获取数据。

    DirectBuffer内存回收

        DirectBuffer内存回收主要有两种方式,一种是通过System.gc来回收,另一种是通过构造函数里创建的Cleaner对象来回收。

    System.gc回收

    1.  
      DirectBuffer的构造函数中,用到了Bit.reserveMemory这个方法,该方法如下
    2.  
       
    1.  
       
    2.  
      static void reserveMemory(long size, int cap) {
    3.  
       
    4.  
      ······
    5.  
       
    6.  
      if (tryReserveMemory(size, cap)) {
    7.  
       
    8.  
      return;
    9.  
       
    10.  
      }
    11.  
       
    12.  
      ······
    13.  
       
    14.  
      while (jlra.tryHandlePendingReference()) {
    15.  
       
    16.  
      if (tryReserveMemory(size, cap)) {
    17.  
       
    18.  
      return;
    19.  
       
    20.  
      }
    21.  
       
    22.  
      }
    23.  
       
    24.  
       
    25.  
       
    26.  
      System.gc();
    27.  
       
    28.  
      // a retry loop with exponential back-off delays
    29.  
       
    30.  
      // (this gives VM some time to do it's job)
    31.  
       
    32.  
      boolean interrupted = false;
    33.  
       
    34.  
      try {
    35.  
       
    36.  
      long sleepTime = 1;
    37.  
       
    38.  
      int sleeps = 0;
    39.  
       
    40.  
      while (true) {
    41.  
       
    42.  
      if (tryReserveMemory(size, cap)) {
    43.  
       
    44.  
      return;
    45.  
       
    46.  
      }
    47.  
       
    48.  
      if (sleeps >= MAX_SLEEPS) {
    49.  
       
    50.  
      break;
    51.  
       
    52.  
      }
    53.  
       
    54.  
      if (!jlra.tryHandlePendingReference()) {
    55.  
       
    56.  
      try {
    57.  
       
    58.  
      Thread.sleep(sleepTime);
    59.  
       
    60.  
      sleepTime <<= 1;
    61.  
       
    62.  
      sleeps++;
    63.  
       
    64.  
      } catch (InterruptedException e) {
    65.  
       
    66.  
      interrupted = true;
    67.  
       
    68.  
      }
    69.  
       
    70.  
      }
    71.  
       
    72.  
      }
    73.  
       
    74.  
      // no luck
    75.  
       
    76.  
      throw new OutOfMemoryError("Direct buffer memory");
    77.  
       
    78.  
      } finally {
    79.  
       
    80.  
      if (interrupted) {
    81.  
       
    82.  
      // don't swallow interrupts
    83.  
       
    84.  
      Thread.currentThread().interrupt();
    85.  
       
    86.  
      }
    87.  
       
    88.  
      }
    89.  
       
    90.  
      }
    91.  
       
    92.  
       

        reserveMemory方法首先尝试分配内存,如果分配成功的话,那么就直接退出。如果分配失败那么就通过调用tryHandlePendingReference来尝试清理堆外内存(最终调用的是Cleaner的clean方法,其实就是unsafe.freeMemory然后释放内存),清理完内存之后再尝试分配内存。如果还是失败,调用System.gc()来触发一次FullGC进行回收(前提是没有加-XX:-+DisableExplicitGC参数)。GC完之后再进行内存分配,失败的话就会进行sleep,然后再进行尝试。每次sleep的时间是逐步增加的,规律是1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)。如果最终还没有可分配的内存,那么就会抛出OOM异常。

        为什么是通过调用tryHandlePendingReference来回收内存呢?答案是JVM在判断内存不可达之后会把需要GC的不可达对象放在一个PendingList中,然后应用程序就可以看到这些对象。通过调用tryHandlePendingReference来访问这些不可达对象。如果不可达对象是Cleaner类型,也就是说关联了堆外的DirectBuffer,那么该DirectBuffer就可以被回收了,通过调用Cleaner的clean方法来回收这部分堆外内存。

    这个逻辑就是进行堆外内存分配时触发的回收内存逻辑,也就是说在分配的时候如果遇到堆外内存不足,可能会触发FullGC,然后尝试进行分配。这也是为什么在一些用到堆外内存的应用中不建议加上-XX:-+DisableExplicitGC参数

    Cleaner对象回收

        另个触发堆外内存回收的时机是通过Cleaner对象的clean方法进行回收。在每次新建一个DirectBuffer对象的时候,会同时创建一个Cleaner对象,同一个进程创建的所有的DirectBuffer对象跟Cleaner对象的个数是一样的,并且所有的Cleaner对象会组成一个链表,前后相连。

    1.  
       
    2.  
      public static Cleaner create(Object ob, Runnable thunk) {
    3.  
       
    4.  
      if (thunk == null)
    5.  
       
    6.  
      return null;
    7.  
       
    8.  
      return add(new Cleaner(ob, thunk));
    9.  
       
    10.  
      }
    11.  
       
    12.  
       

        Cleaner对象的clean方法执行时机是JVM在判断该Cleaner对象关联的DirectBuffer已经不被任何对象引用了(也就是经过可达性分析判定为不可达的时候)。此时Cleaner对象会被JVM挂到PendingList上。然后有一个固定的线程扫描这个List,如果遇到Cleaner对象,那么就执行clean方法。

          DirectBuffer在一些高性能的中间件上使用还是相当广泛的。正确的使用可以提升程序的性能,降低GC的频率。

    ----------------------------------------------------------------------------------------------

    欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。

  • 相关阅读:
    Python 类中方法的内部变量,命名加'self.'变成 self.xxx 和不加直接 xxx 的区别
    用foreach遍历 datagridView 指定列所有的内容
    treeView1.SelectedNode.Level
    YES NO 上一个 下一个
    正则 单词全字匹配查找 reg 边界查找 精确匹配 只匹配字符 不含连续的字符
    抓取2个字符串中间的字符串
    sqlite 60000行 插入到数据库只用不到2秒
    将多行文本以单行的格式保存起来 读和写 ini
    将秒转换成时间格式
    richtextbox Ctrl+V只粘贴纯文本格式
  • 原文地址:https://www.cnblogs.com/tabCtrlShift/p/9337582.html
Copyright © 2011-2022 走看看