堆外内存

堆外内存定义java

  建立Java.nio.DirectByteBuffer时分配的内存。缓存

堆外内存优缺点框架

  优势: 提高了IO效率(避免了数据从用户态向内核态的拷贝);减小了GC次数(节约了大量的堆内内存)。性能

  缺点:分配和回收堆外内存比分配和回收堆内存耗时;(解决方案:经过对象池避免频繁地建立和销毁堆外内存)线程

为何堆外内存可以提高IO效率?调试

  堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。若是从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,而后再由OS写入磁盘,使用堆外内存避免了数据从用户内向内核态的拷贝。日志

堆外内存申请netty

  JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层经过unsafe.allocateMemory(size)实现。Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。对象

堆外内存释放blog

  JDK中使用DirectByteBuffer对象来表示堆外内存,每一个DirectByteBuffer对象在初始化时,都会建立一个对应的Cleaner对象,用于保存堆外内存的元信息(开始地址、大小和容量等),当DirectByteBuffer被GC回收后,Cleaner对象被放入ReferenceQueue中,而后由ReferenceHandler守护线程调用unsafe.freeMemory(address),回收堆外内存。

 主动回收(推荐): 对于Sun的JDK,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,而后调用它的clean()就行;

  基于 GC 回收:堆内的DirectByteBuffer对象被GC时,会调用cleaner回收其引用的堆外内存。问题是YGC只会将将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收,若是有大量的DirectByteBuffer对象移到了old区,可是又一直没有作CMS GC或者FGC,而只进行YGC,物理内存会被慢慢耗光,触发OOM;

堆外内存溢出

  Java.nio.DirectByteBuffer所需的内存超过了物理分配的堆外内存,出现”java.lang.OutOfMemoryError: Direct buffer memory”。

堆外内存使用注意

  java.nio.DirectByteBuffer对象在建立过程当中会先经过Unsafe接口直接经过os::malloc来分配内存,而后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就能够直接操做这些内存。这些内存只有在DirectByteBuffer回收掉以后才有机会被回收,所以若是这些对象大部分都移到了old,可是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,由于你的物理内存被他们耗尽了,所以为了不这种悲剧的发生,经过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来作一次full gc,以此来回收掉没有被使用的堆外内存。

堆外内存默认大小

  堆外内存默认值: (-Xmx值) - (1个survivor大小)

为何Cleaner对象可以被放入ReferenceQueue中?

  Cleaner对象关联了一个PhantomReference引用,若是GC过程当中某个对象除了只有PhantomReference引用它以外,并无其余地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在GC完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,在最终的处理里会经过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。

 

Java的虚拟机对内存的管理大部分状况下就是指堆内存的管理, GC的也是对堆内存的清理和回收.

 

 netty中提到的一个概念, "零拷贝", 也是netty高性能的缘由之一. 零拷贝, 主要体如今三个方面:

    Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外(直接)内存进行Socket读写,不须要进行字节缓冲区的二次拷贝。若是使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内Buffer拷贝一份到直接内存中,而后才写入Socket中。相比于堆外直接内存,消息在发送过程当中多了一次缓冲区的内存拷贝。

    Netty提供了组合Buffer对象,能够聚合多个ByteBuffer对象,用户能够像操做一个Buffer那样方便的对组合Buffer进行操做,避免了传统经过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

    Netty的文件传输采用了transferTo方法,它能够直接将文件缓冲区的数据发送到目标Channel,避免了传统经过循环write方式致使的内存拷贝问题。

在使用堆外内存的同时也带来了新的问题, 相比较堆内存, 堆外内存的分配和回收要更耗时, 因此netty提供了基于内存池的缓冲区重用机制.

 

将本地缓存和堆外内存联系到一块儿, 是有一次调试线上频繁FULL GC而后OOM的问题, 当时的状况是, 线上频繁的报警有FULLGC, 怀疑有内存泄露,, 经过dump堆内存快照, 分析后发现有一个特别大的HashMap, 缘由是打点日志引发的, 应用默认集成了日志系统会自动记录用户的全部行为, 应用这边合并日志后发送到日志接收端, 日志接收端挂掉了, 致使一直在重试, 重试的过程当中不停的有新的日志加进来, 最后致使FULLGC. 当时我就在想, 若是将这种本地缓存移到堆外是否是就能够不用参与GC, 也可使用更大的内存.

堆外内存有如下特色:

    对于大内存有良好的伸缩性, 堆外内存突破JVM的内存限制

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

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

堆外内存更适合生命周期中等或长期的对象

 

关于堆外内存的回收

堆外内存的回收其实依赖于咱们的GC机制(堆外内存不会对GC形成什么影响)

首先咱们要知道在java层面和咱们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和GC也有关,那就是GC能经过操做DirectByteBuffer对象来间接操做对应的堆外内存了。

DirectByteBuffer对象在建立的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象什么时候被回收的,它不能影响GC决策.

GC过程当中若是发现某个对象除了只有PhantomReference引用它以外,并无其余的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在GC完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理, 而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会经过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块

 

    什么是基地址 这里就提出了段的概念, 将1G的数据划分为n个段, 每个段是64K, 每个段也就是每个64K就是一个基地址 段内的数据的地址就是当前基地址的偏移地址, 此时 段地址+偏移地址就可以找到真正的内存数据了.

 

为何要主动调用System.gc

System.gc()会对新生代的老生代都会进行内存回收,这样会比较完全地回收DirectByteBuffer对象以及他们关联的堆外内存.

DirectByteBuffer对象自己实际上是很小的,可是它后面可能关联了一个很是大的堆外内存,所以咱们一般称之为*冰山对象.

咱们作ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,可是没法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是咱们一般碰到的最大的问题.

若是有大量的DirectByteBuffer对象移到了old,可是又一直没有作cms gc或者full gc,而只进行ygc,那么咱们的物理内存可能被慢慢耗光,可是咱们还不知道发生了什么,由于heap明明剩余的内存还不少(前提是咱们禁用了System.gc -- JVM参数DisableExplicitGC)。

相关文章
相关标签/搜索