使用Java语言的同窗们都知道, Java的虚拟机对内存的管理大部分状况下就是指堆内存的管理, GC的也是对堆内存的清理和回收. 下面就看一下堆外内存的对JVM的意义.java
第一次了解到堆外内存的使用场景是在使用netty, netty中提到的一个概念, "零拷贝", 也是netty高性能的缘由之一. 零拷贝, 主要体如今三个方面:缓存
在使用堆外内存的同时也带来了新的问题, 相比较堆内存, 堆外内存的分配和回收要更耗时, 因此netty提供了基于内存池的缓冲区重用机制.服务器
将本地缓存和堆外内存联系到一块儿, 是有一次调试线上频繁FULL GC而后OOM的问题, 当时的状况是, 线上频繁的报警有FULLGC, 怀疑有内存泄露,, 经过dump堆内存快照, 分析后发现有一个特别大的HashMap, 缘由是打点日志引发的, 应用默认集成了日志系统会自动记录用户的全部行为, 应用这边合并日志后发送到日志接收端, 日志接收端挂掉了, 致使一直在重试, 重试的过程当中不停的有新的日志加进来, 最后致使FULLGC. 当时我就在想, 若是将这种本地缓存移到堆外是否是就能够不用参与GC, 也可使用更大的内存.性能
堆外内存有如下特色:线程
堆外内存更适合生命周期中等或长期的对象调试
关于堆外内存的回收
堆外内存的回收其实依赖于咱们的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就是一个基地址 段内的数据的地址就是当前基地址的偏移地址, 此时 段地址+偏移地址就可以找到真正的内存数据了.netty
为何要主动调用System.gc
System.gc()会对新生代的老生代都会进行内存回收,这样会比较完全地回收DirectByteBuffer对象以及他们关联的堆外内存.
DirectByteBuffer对象自己实际上是很小的,可是它后面可能关联了一个很是大的堆外内存,所以咱们一般称之为**冰山对象*.
咱们作ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,可是没法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是咱们一般碰到的最大的问题.
若是有大量的DirectByteBuffer对象移到了old,可是又一直没有作cms gc或者full gc,而只进行ygc,那么咱们的物理内存可能被慢慢耗光,可是咱们还不知道发生了什么,由于heap明明剩余的内存还不少(前提是咱们禁用了System.gc -- JVM参数DisableExplicitGC)。code
JVM GC-Invisible Heap
淘宝基于OpenJDK HotSpot VM,定制且开源的服务器版Java虚拟机. GC-Invisible Heap,简称GCIH,是一种将Java对象从Java堆内移动到堆外,而且能够在JVM间共享这些对象的技术。对象
如何从JVM移入堆外内存
遍历全部对象, 从根对象开始, 根据对象的引用关系递归调用moveIn(将该对象在GCIH的地址encode到刻对象的header上, 并设置header的最低2位为01).
如何阻止GC扫描 正常的GC过程是, 从root对象出发, 标记活的对象的同时, 将这个对象push到queue, 以便后续的GC线程处理 GCIH更改了GC过程, 当某个对象的地址在GCIH地址空间内时, 直接返回, 不将对象放到queue中