• 转 深入理解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程序员必备。

  • 相关阅读:
    150个JS特效脚本
    .sql文件被加密恢复
    Alpha865qqz.id 加密数据库恢复
    最新incaseformat 病毒删除文件恢复
    Oracle MISSING00000文件故障恢复
    asm 磁盘分区丢失恢复----惜分飞
    oracle数据文件0kb恢复
    ORA-600 16703--oracle介质被注入恶意脚本
    GANDCRAB病毒oracle数据库恢复
    文件系统损坏,oracle数据库恢复
  • 原文地址:https://www.cnblogs.com/tabCtrlShift/p/9337582.html
Copyright © 2020-2023  润新知