Java网络编程和NIO详解8:浅析mmap和Direct Buffer

Java网络编程与NIO详解8:浅析mmap和Direct Buffer

本系列文章首发于个人我的博客:https://h2pl.github.io/html

欢迎阅览个人CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.htmljava

部分代码会放在个人的Github:https://github.com/h2pl/node

 

 

Java网络编程与NIO详解8:浅析mmap和Direct Buffer

以前看到一篇文章说epoll中在维护epoll句柄数据结构使用到了mmap的技术,可是后来看了其余文章以及epoll源码后发现好像并无用到这个技术linux

转自知乎:git

epoll_wait的实现~有关从内核态拷贝到用户态代码.能够看到__put_user这个函数就是内核拷贝到用户空间.分析完整个linux 2.6版本的epoll实现没有发现使用了mmap系统调用,根本不存在共享内存在epoll的实现github

if (revents) {
           /* 将当前的事件和用户传入的数据都copy给用户空间,
            * 就是epoll_wait()后应用程序能读到的那一堆数据. */
           if (__put_user(revents, &uevent->events) ||
               __put_user(epi->event.data, &uevent->data)) {
               /* 若是copy过程当中发生错误, 会中断链表的扫描,
                * 并把当前发生错误的epitem从新插入到ready list.
                * 剩下的没处理的epitem也不会丢弃, 在ep_scan_ready_list()
                * 中它们也会被从新插入到ready list */
               list_add(&epi->rdllink, head);
               return eventcnt ? eventcnt : -EFAULT;
          }

那么既然提到了,就让咱们看看mmap究竟是什么吧spring

mmap:内存映射文件

转自:https://www.cnblogs.com/huxiao-tee/p/4660352.html[](http://projects.spring.io/spring-boot)编程

mmap基础概念

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就能够采用指针的方式读写操做这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操做而没必要再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而能够实现不一样进程间的文件共享。以下图所示:缓存

由上图能够看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具备一样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。网络

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,因为每一个不一样质的虚拟内存区域功能和内部机制都不一样,所以一个进程使用多个vm_area_struct结构来分别表示不一样类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构连接,方便进程快速访问,以下图所示:

vm_area_struct结构中包含区域起始和终止地址以及其余相关信息,同时也包含一个vm_ops指针,其内部可引出全部针对这个区域可使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操做须要用要的信息,均可以从vm_area_struct中得到。mmap函数就是要建立一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。具体步骤请看下一节。

回到顶部

mmap内存映射原理

mmap内存映射的实现过程,总的来讲能够分为三个阶段:

(一)进程启动映射过程,并在虚拟地址空间中为映射建立虚拟映射区域

一、进程在用户空间调用库函数mmap,原型:void mmap(void start, size_t length, int prot, int flags, int fd, off_t offset);

二、在当前进程的虚拟地址空间中,寻找一段空闲的知足要求的连续的虚拟地址

三、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化

四、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树

(二)调用内核空间的系统调用函数mmap(不一样于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

五、为映射分配了新的虚拟地址区域后,经过待映射的文件指针,在文件描述符表中找到对应的文件描述符,经过文件描述符,连接到内核“已打开文件集”中该文件的文件结构体(struct file),每一个文件结构体维护着和这个已打开文件相关各项信息。

六、经过该文件的文件结构体,连接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file filp, struct vm_area_struct vma),不一样于用户空间库函数。

七、内核mmap函数经过虚拟文件系统inode模块定位到文件磁盘物理地址

八、经过remap_pfn_range函数创建页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并无任何数据关联到主存中

(三)进程发起对这片映射空间的访问,引起缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于建立虚拟区间并完成地址映射,可是并无将任何文件数据的拷贝至主存真正的文件读取是当进程发起读或写操做时

九、进程的读或写操做访问虚拟地址空间这一段映射地址,经过查询页表,发现这一段地址并不在物理页面上。由于目前只创建了地址映射,真正的硬盘数据尚未拷贝到内存中,所以引起缺页异常

十、缺页异常进行一系列判断,肯定无非法操做后,内核发起请求调页过程

十一、调页过程先在交换缓存空间(swap cache)中寻找须要访问的内存页,若是没有则调用nopage函数把所缺的页从磁盘装入到主存中。

十二、以后进程即可对这片主存进行读或者写的操做,若是写操做改变了其内容,必定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

注:修改过的脏页面并不会当即更新回文件中,而是有一段时间的延迟,能够调用msync()来强制同步, 这样所写的内容就能当即保存到文件里了。

回到顶部

mmap和常规文件操做的区别

对linux文件系统不了解的朋友,请参阅我以前写的博文《从内核文件系统看文件读写过程》,咱们首先简单的回顾一下常规文件系统操做(调用read/fread等类函数)中,函数的调用过程:

一、进程发起读文件请求

二、内核经过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode

三、inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。若是存在,则直接返回这片文件页的内容。

四、若是不存在,则经过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。以后再次发起读页面过程,进而将页缓存中的数据发给用户进程。

总结来讲,常规文件操做为了提升读写效率和保护磁盘,使用了页缓存机制。这样形成读文件时须要先将文件页从磁盘拷贝到页缓存中,因为页缓存处在内核空间不能被用户进程直接寻址,因此还须要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,经过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操做也是同样,待写入的buffer在内核空间不能直接访问,必需要先拷贝至内核空间对应的主存再写回磁盘中(延迟写回),也是须要两次数据拷贝

而使用mmap操做文件中,建立新的虚拟内存区域和创建文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操做。而以后访问数据时发现内存中并没有数据而发起的缺页异常过程,能够经过已经创建好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操做须要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只须要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不一样数据不通的繁琐过程。所以mmap效率更高。

回到顶部

mmap优势总结

由上文讨论可知,mmap优势共有一下几点:

一、对文件的读取操做跨过了页缓存,减小了数据的拷贝次数,用内存读写取代I/O读写,提升了文件读取效率。

二、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操做能够直接反映在映射的区域内,从而被对方空间及时捕捉

三、提供进程间共享内存及相互通讯的方式。无论是父子进程仍是无亲缘关系的进程,均可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而经过各自对映射区域的改动,达到进程间通讯和进程间共享的目的。

同时,若是进程A和进程B都映射了区域C,当A第一次读取C时经过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,可是再也不须要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

四、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操做的一个方面,解决方案每每是借助硬盘空间协助操做,补充内存的不足。可是进一步会形成大量的文件I/O操做,极大影响效率。这个问题能够经过mmap映射很好的解决。换句话说,但凡是须要用磁盘空间代替内存的时候,mmap均可以发挥其功效。

堆外内存之 DirectByteBuffer 详解

原文出处: tomas家的小拨浪鼓

堆外内存

堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,咱们平时在Java中建立的对象都处于堆内内存中,而且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控以外的一块内存区域,所以它是不受JVM的管控。

在讲解DirectByteBuffer以前,须要先简单了解两个知识点。

java引用类型,由于DirectByteBuffer是经过虚引用(Phantom Reference)来实现堆外内存的释放的。

PhantomReference 是全部“弱引用”中最弱的引用类型。不一样于软引用和弱引用,虚引用没法经过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码能够发现 get() 被重写为永远返回 null

那虚引用到底有什么做用?其实虚引用主要被用来 跟踪对象被垃圾回收的状态,经过查看引用队列**中是否包含对象所对应的虚引用来判断它是否** 即将被垃圾回收,从而采起行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的做用

关于java引用类型的实现和原理能够阅读以前的文章Reference 、ReferenceQueue 详解Java 引用类型简述

关于linux的内核态和用户态

4235178-7c5ca2cb236fd2eb

 

  • 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。好比socket I/0操做或者文件的读写操做等

  • 用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。

  • 系统调用:为了使上层应用可以访问到这些资源,内核为上层应用提供访问的接口。

4235178-2393d0797135217b

 

所以咱们能够得知当咱们经过JNI调用的native方法实际上就是从用户态切换到了内核态的一种方式。而且经过该系统调用使用操做系统所提供的功能。

Q:为何须要用户进程(位于用户态中)要经过系统调用(Java中即便JNI)来调用内核态中的资源,或者说调用操做系统的服务了? A:intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态Ring0做为内核态Ring3状态不能访问Ring0的地址空间,包括代码和数据。所以用户态是没有权限去操做内核态的资源的,它只能经过系统调用外完成用户态到内核态的切换,而后在完成相关操做后再由内核态切换回用户态

DirectByteBuffer ———— 直接缓冲

DirectByteBuffer是Java用于实现堆外内存的一个重要类,咱们能够经过该类实现堆外内存的建立、使用和销毁

4235178-fc2ae3eac18813d3

 

DirectByteBuffer该类自己还是位于Java内存模型的堆中。堆内内存是JVM能够直接管控、操纵。

而DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,经过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,因此在DirectByteBuffer必定会存在某种方式来操纵堆外内存

在DirectByteBuffer的父类Buffer中有个address属性:

// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;

address只会被直接缓存给使用到。之因此将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提高它调用的速率。 address表示分配的堆外内存的地址。

4235178-5e010dc58916b102

 

unsafe.allocateMemory(size);分配完堆外内存后就会返回分配的堆外内存基地址,并将这个地址赋值给了address属性。这样咱们后面经过JNI对这个堆外内存操做时都是经过这个address来实现的了。

在前面咱们说过,在linux中内核态的权限是最高的,那么在内核态的场景下,操做系统是能够访问任何一个内存区域的,因此操做系统是能够访问到Java堆的这个内存区域的。

Q:那为何操做系统不直接访问Java堆内的内存区域了? A:这是由于JNI(java native interface 提供若干个API实现java和其余语言的通讯)方法访问的内存区域是一个已经肯定了的内存区域地质,那么该内存地址指向的是Java堆内内存的话,那么若是在操做系统正在访问这个内存地址的时候,Java在这个时候进行了GC操做,而GC操做会涉及到数据的移动操做[GC常常会进行先标志在压缩的操做。即,将可回收的空间作标志,而后清空标志位置的内存,而后会进行一个压缩,压缩就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。因此JNI调用的内存是不能进行GC操做的。

Q:如上面所说,JNI调用的内存是不能进行GC操做的,那该如何解决了? A:①堆内内存与堆外内存之间数据拷贝的方式(而且在将堆内内存拷贝到堆外内存的过程JVM会保证不会进行GC操做):好比咱们要完成一个从文件中读数据到堆内内存的操做,即FileChannelImpl.read(HeapByteBuffer)。这里实际上File I/O会将数据读到堆外内存中,而后堆外内存再将数据拷贝到堆内内存,这样咱们就读到了文件中的内存

4235178-f94db8df14023550

 

static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
      if (var1.isReadOnly()) {
          throw new IllegalArgumentException("Read-only buffer");
      } else if (var1 instanceof DirectBuffer) {
          return readIntoNativeBuffer(var0, var1, var2, var4);
      } else {
          // 分配临时的堆外内存
          ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());

          int var7;
          try {
              // File I/O 操做会将数据读入到堆外内存中
              int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
              var5.flip();
              if (var6 > 0) {
                  // 将堆外内存的数据拷贝到堆外内存中
                  var1.put(var5);
              }

              var7 = var6;
          } finally {
              // 里面会调用DirectBuffer.cleaner().clean()来释放临时的堆外内存
              Util.offerFirstTemporaryDirectBuffer(var5);
          }

          return var7;
      }
  }

写操做则反之,咱们会将堆内内存的数据先写到对堆外内存中,而后操做系统会将堆外内存的数据写入到文件中

直接使用堆外内存,如DirectByteBuffer:这种方式是直接在堆外分配一个内存(即,native memory)来存储数据,程序经过JNI直接将数据读/写到堆外内存中。由于数据直接写入到了堆外内存中,因此这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操做了。这样在进行I/O操做时,只须要将这个堆外内存地址传给JNI的I/O的函数就行了。

DirectByteBuffer堆外内存的建立和回收的源码解读

堆外内存分配

DirectByteBuffer(int cap) {                   // package-private
      super(-1, 0, cap, cap);
      boolean pa = VM.isDirectMemoryPageAligned();
      int ps = Bits.pageSize();
      long size = Math.max(1L, (long)cap + (pa ? ps : 0));
      // 保留总分配内存(按页分配)的大小和实际内存的大小
      Bits.reserveMemory(size, cap);

      long base = 0;
      try {
          // 经过unsafe.allocateMemory分配堆外内存,并返回堆外内存的基地址
          base = unsafe.allocateMemory(size);
      } catch (OutOfMemoryError x) {
          Bits.unreserveMemory(size, cap);
          throw x;
      }
      unsafe.setMemory(base, size, (byte) 0);
      if (pa && (base % ps != 0)) {
          // Round up to page boundary
          address = base + ps - (base & (ps - 1));
      } else {
          address = base;
      }
      // 构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
      cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
      att = null;
  }

Bits.reserveMemory(size, cap) 方法

static void reserveMemory(long size, int cap) {

  if (!memoryLimitSet && VM.isBooted()) {
      maxMemory = VM.maxDirectMemory();
      memoryLimitSet = true;
  }

  // optimist!
  if (tryReserveMemory(size, cap)) {
      return;
  }

  final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

  // retry while helping enqueue pending Reference objects
  // which includes executing pending Cleaner(s) which includes
  // Cleaner(s) that free direct buffer memory
  while (jlra.tryHandlePendingReference()) {
      if (tryReserveMemory(size, cap)) {
          return;
      }
  }

  // trigger VM's Reference processing
  System.gc();

  // a retry loop with exponential back-off delays
  // (this gives VM some time to do it's job)
  boolean interrupted = false;
  try {
      long sleepTime = 1;
      int sleeps = 0;
      while (true) {
          if (tryReserveMemory(size, cap)) {
              return;
          }
          if (sleeps >= MAX_SLEEPS) {
              break;
          }
          if (!jlra.tryHandlePendingReference()) {
              try {
                  Thread.sleep(sleepTime);
                  sleepTime <<= 1;
                  sleeps++;
              } catch (InterruptedException e) {
                  interrupted = true;
              }
          }
      }

      // no luck
      throw new OutOfMemoryError("Direct buffer memory");

  } finally {
      if (interrupted) {
          // don't swallow interrupts
          Thread.currentThread().interrupt();
      }
  }
}

该方法用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。

其中,若是系统中内存( 即,堆外内存 )不够的话:

SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
          @Override
          public boolean tryHandlePendingReference() {
              return tryHandlePending(false);
          }
      });

jlra.tryHandlePendingReference()会触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。 由于在Reference的静态代码块中定义了:

SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
          @Override
          public boolean tryHandlePendingReference() {
              return tryHandlePending(false);
          }
      });

若是在进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则

// trigger VM's Reference processing
System.gc();

System.gc()会触发一个full gc,固然前提是你没有显示的设置-XX:+DisableExplicitGC来禁用显式GC。而且你须要知道,调用System.gc()并不可以保证full gc立刻就能被执行。

因此在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。而且每次尝试以前,都对延迟等待时间,已给JVM足够的时间去完成full gc操做。若是9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError(“Direct buffer memory”)异常。

4235178-6da0d60191992f59

 

注意,这里之因此用使用full gc的很重要的一个缘由是:System.gc()会对新生代和老生代都会进行内存回收,这样会比较完全地回收DirectByteBuffer对象以及他们关联的堆外内存. DirectByteBuffer对象自己实际上是很小的,可是它后面可能关联了一个很是大的堆外内存,所以咱们一般称之为冰山对象.

咱们作ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,可是没法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是咱们一般碰到的最大的问题。( 而且堆外内存多用于生命期中等或较长的对象 ) 若是有大量的DirectByteBuffer对象移到了old,可是又一直没有作cms gc或者full gc,而只进行ygc,那么咱们的物理内存可能被慢慢耗光,可是咱们还不知道发生了什么,由于heap明明剩余的内存还不少(前提是咱们禁用了System.gc – JVM参数DisableExplicitGC)。

总的来讲,Bits.reserveMemory(size, cap)方法在可用堆外内存不足以分配给当前要建立的堆外内存大小时,会实现如下的步骤来尝试完成本次堆外内存的建立:

① 触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。 ② 若是进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()。System.gc()会触发一个full gc,但你须要知道,调用System.gc()并不可以保证full gc立刻就能被执行。因此在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。而且每次尝试以前,都对延迟等待时间,已给JVM足够的时间去完成full gc操做。 注意,若是你设置了-XX:+DisableExplicitGC,将会禁用显示GC,这会使System.gc()调用无效。 ③ 若是9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError(“Direct buffer memory”)异常。

那么可用堆外内存究竟是多少了?,即默认堆外存内存有多大: ① 若是咱们没有经过-XX:MaxDirectMemorySize来指定最大的堆外内存。则 ② 若是咱们没经过-Dsun.nio.MaxDirectMemorySize指定了这个属性,且它不等于-1。则 ③ 那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法

JNIEXPORT jlong JNICALL
Java_java_lang_Runtime_maxMemory(JNIEnv *env, jobject this)
{
    return JVM_MaxMemory();
}

JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
  JVMWrapper("JVM_MaxMemory");
  size_t n = Universe::heap()->max_capacity();
  return convert_size_t_to_jlong(n);
JVM_END

其中在咱们使用CMS GC的状况下也就是咱们设置的-Xmx的值里除去一个survivor的大小就是默认的堆外内存的大小了。

堆外内存回收

Cleaner是PhantomReference的子类,并经过自身的next和prev字段维护的一个双向链表。PhantomReference的做用在于跟踪垃圾回收过程,并不会对对象的垃圾回收过程形成任何的影响。 因此cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 用于对当前构造的DirectByteBuffer对象的垃圾回收过程进行跟踪。 当DirectByteBuffer对象从pending状态 ——> enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现经过unsafe对堆外内存的释放。

4235178-792afac32aefd061

 

4235178-07eaab88f1d02927

 

虽然Cleaner不会调用到Reference.clear(),但Cleaner的clean()方法调用了remove(this),即将当前Cleaner从Cleaner链表中移除,这样当clean()执行完后,Cleaner就是一个无引用指向的对象了,也就是可被GC回收的对象。

thunk方法:

4235178-ebeffa00197df134

 

经过配置参数的方式来回收堆外内存

同时咱们能够经过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc()来作一次full gc,以此来回收没有被使用的堆外内存

堆外内存那些事

使用堆外内存的缘由

  • 对垃圾回收停顿的改善

由于full gc 意味着完全回收,完全回收时,垃圾收集器会对全部分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用形成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。若是使用堆外内存的话,堆外内存是直接受操做系统管理( 而不是虚拟机 )。这样作的结果就是能保持一个较小的堆内内存,以减小垃圾收集对应用的影响。

  • 在某些场景下能够提高程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。

什么状况下使用堆外内存

  • 堆外内存适用于生命周期中等或较长的对象。( 若是是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用形成的性能影响 )。

  • 直接的文件拷贝操做,或者I/O操做。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操做,由于I/O操做是系统内核内存和设备间的通讯,而不是经过程序直接和外设通讯的。

  • 同时,还可使用 内存池+堆外内存 的组合方式,来对生命周期较短,但涉及到I/O操做的对象进行堆外内存的再使用。( Netty中就使用了该方式 )

堆外内存 VS 内存池

  • 内存池:主要用于两类对象:①生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增长CPU缓存的命中率,从而提升性能;②加载含有大量重复对象的大片数据,此时使用内存池能减小垃圾回收的时间。

  • 堆外内存:它和内存池同样,也能缩短垃圾回收时间,可是它适用的对象和内存池彻底相反。内存池每每适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。

堆外内存的特色

  • 对于大内存有良好的伸缩性

  • 垃圾回收停顿的改善能够明显感受到

  • 进程间能够共享,减小虚拟机间的复制

堆外内存的一些问题

  • 堆外内存回收问题,以及堆外内存的泄漏问题。这个在上面的源码解析已经提到了

  • 堆外内存的数据结构问题:堆外内存最大的问题就是你的数据结构变得不那么直观,若是数据结构比较复杂,就要对它进行串行化(serialization),而串行化自己也会影响性能。另外一个问题是因为你可使用更大的内存,你可能开始担忧虚拟内存(即硬盘)的速度对你的影响了。

参考

相关文章
相关标签/搜索