• 堆外内存总结


    前言

      上次网易一面面试官提到了“是否了解堆外内存?”、“堆外内存是否需要手动释放?”等问题,那时候我误以为所提到的“堆外内存”是指元空间这个jvm管理的堆外内存,对于元空间是否手动释放这样的问题就令我十分疑惑,按理说当元空间的类信息会在类被定义成“无用的类”时会被回收,因此不需要我们手动释放,然后面试小哥又重复了一遍我的回答“不需要手动释放吗?”,我只能回答对此可能不是很了解。

      面试结束后上网搜索了一下,他想要问的应该是java中的DirectByteBuffer,而今天早上又看到一篇博客“堆外内存泄漏的排查过程”。就打算今天对堆外内存做一个总结。

    使用

      DirectByteBuffer的创建非常简单,使用ByteBuffer的静态方法

    1     ByteBuffer dirBuf = ByteBuffer.allocateDirect(capacity);

      就可以创建一个DirectByteBuffer,与普通的ByteBuffer不一样的地方在于一个是在jvm堆内存中,另一个不在jvm堆内存中。

    创建、清理

      为了了解堆外内存是如何被回收的,我们先来看allocateDirect这个方法是如何创建一个实例的。

     1     DirectByteBuffer(int cap) {                   // package-private
     2 
     3         super(-1, 0, cap, cap);
     4         boolean pa = VM.isDirectMemoryPageAligned();
     5         int ps = Bits.pageSize();
     6         long size = Math.max(1L, (long)cap + (pa ? ps : 0));
     7         Bits.reserveMemory(size, cap);//1.1预定一块空间
     8 
     9         long base = 0;
    10         try {
    11             base = unsafe.allocateMemory(size);//1.2创建
    12         } catch (OutOfMemoryError x) {
    13             Bits.unreserveMemory(size, cap);
    14             throw x;
    15         }
    16         unsafe.setMemory(base, size, (byte) 0);
    17         if (pa && (base % ps != 0)) {
    18             // Round up to page boundary
    19             address = base + ps - (base & (ps - 1));
    20         } else {
    21             address = base;
    22         }
    23         cleaner = Cleaner.create(this, new Deallocator(base, size, cap));//2.构造一个Cleaner对象
    24         att = null;
    25 
    26     }

      先进入(注释1.1)Bits.reserveMemory(size, cap)这个方法,它主要是预申请一块空间,size是系统的页大小。

     1     static void reserveMemory(long size, int cap) {
     2 
     3         if (!memoryLimitSet && VM.isBooted()) {
     4             maxMemory = VM.maxDirectMemory();
     5             memoryLimitSet = true;
     6         }
     7 
     8         // optimist!
     9         if (tryReserveMemory(size, cap)) {
    10             return;
    11         }
    12 
    13         final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
    14 
    15         // retry while helping enqueue pending Reference objects
    16         // which includes executing pending Cleaner(s) which includes
    17         // Cleaner(s) that free direct buffer memory
    18         while (jlra.tryHandlePendingReference()) {
    19             if (tryReserveMemory(size, cap)) {
    20                 return;
    21             }
    22         }
    23 
    24         // trigger VM's Reference processing
    25         System.gc();
    26 
    27         // a retry loop with exponential back-off delays
    28         // (this gives VM some time to do it's job)
    29         boolean interrupted = false;
    30         try {
    31             long sleepTime = 1;
    32             int sleeps = 0;
    33             while (true) {
    34                 if (tryReserveMemory(size, cap)) {
    35                     return;
    36                 }
    37                 if (sleeps >= MAX_SLEEPS) {
    38                     break;
    39                 }
    40                 if (!jlra.tryHandlePendingReference()) {
    41                     try {
    42                         Thread.sleep(sleepTime);
    43                         sleepTime <<= 1;
    44                         sleeps++;
    45                     } catch (InterruptedException e) {
    46                         interrupted = true;
    47                     }
    48                 }
    49             }
    50 
    51             // no luck
    52             throw new OutOfMemoryError("Direct buffer memory");
    53 
    54         } finally {
    55             if (interrupted) {
    56                 // don't swallow interrupts
    57                 Thread.currentThread().interrupt();
    58             }
    59         }
    60     }

      reserveMemory方法流程:

      1、首先是第八行的tryReserveMemory方法,尝试申请空间

      2、申请失败就到第18-22行尝试清理堆外内存,再tryReserveMemory

      3、如果清理完了还是申请失败,就调用System.gc(),触发full gc,触发后,可以清理老年代(新生代也可能有但是大多是在老年代的)的堆外内存的引用(如果存在应该被清理的引用),清理完后就是剩下的部分,33-53行自旋MAX_SLEEPS次,sleep等待full gc的触发(System.gc()可能有延时),如果次数大于MAX_SLEEPS还没申请成功,就抛出异常。

      

      接下来是(注释1.2)的unsafe.allocateMemory真正创建堆外内存空间。

      再之后是(注释2)创建Cleaner对象,用于之后清理这块堆外内存空间。

      Cleaner继承PhantomReference类,并通过自身的next和prev字段维护的一个双向链表,当DirectByteBuffer对象从“pending” 变为 “enqueue”时(即gc过程中对象只有被虚引用,这个引用会被放到java.lang.ref.Reference.pending队列里,调用ReferenceHandler的run中不断自旋的tryHandlePending(true)方法处理,清理pending链,使用clean方法将堆外内存清理掉)。

    参数设置

      我们可以通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc()来做一次full gc,以此来回收掉没有被使用的堆外内存,这个是jvm底层帮我们做的,我们只需要设定其参数即可。

    使用情景

      1、直接的文件拷贝操作,或者I/O操作。当操作系统对堆内内存进行文件拷贝、io处理时先会拷贝一份到堆外,再进行发送处理,而堆外内存就少了一个拷贝的耗时。

      2、堆外内存适用于生命周期较长的对象,不会占用堆内的内存,这点与元空间的出现原因类似,Class信息一般生命周期也都较长。

  • 相关阅读:
    点击链接,取得href的值,但是不转向
    启动和停止MySQL服务
    QuickText for Notepad++
    otepad++ 配置 支持jquery、html、css、javascript、php代码提示
    windows 2008+IIS7+Mysql+PHP5.5 + FastCGI环境配置
    Jquery异步请求数据实例代码
    JS读取本地文件及目录的方法
    c#.net从ftp下载文件到本地
    怎样才能充分利用SQL索引
    jQuery EasyUI Datagrid性能优化专题
  • 原文地址:https://www.cnblogs.com/zzzdp/p/9598676.html
Copyright © 2020-2023  润新知