堆外内存总结

时间:2021-04-17 15:08:39

前言

  上次网易一面面试官提到了“是否了解堆外内存?”、“堆外内存是否需要手动释放?”等问题,那时候我误以为所提到的“堆外内存”是指元空间这个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信息一般生命周期也都较长。