JVM可以使用的内存分外2种:堆内存和堆外内存.
参考:http://www.jianshu.com/p/84b175a14323(你假笨)
http://calvin1978.blogcn.com/articles/directbytebuffer.html(江南白衣)
堆外内存的创建
可以通过jdk nio中的ByteBuffer创建。如下:
而真正的内存分配是使用的Bits.reserveMemory方法,如下:
在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限 -- 堆外内存的限额默认与堆内内存(由-Xmx 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。
如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出大家最头痛的OOM异常。
最后,创建一个Cleaner,并把代表清理动作的Deallocator类绑定 -- 降低Bits里的totalCapacity,并调用Unsafe调free去释放内存。
堆外内存的回收
存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。
快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。
这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。
这时,就只能靠前面提到的申请额度超限时触发的System.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。还有,万一大家设置了-DisableExplicitGC禁止了system.gc(),那就无法回收了。
所以,堆外内存还是自己主动点回收更好,比如Netty就是这么做的。
Cleaner如何与GC相关联?
DirectByteBuffer中有个成员变量Cleaner,Cleaner是PhantomReference(虚引用)的子类,PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理。如果是Cleaner类型,则执行clean方法,释放堆外内存。
ReferenceHandler是抽象类Reference的一个内部类,代码如下:
为什么要使用堆外内存
(1)可以扩展至更大的内存空间
(2)在进行网络通信的时候,堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中
PS:如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险。