入口ByteBuffer.allocateDirect
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
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 { 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 = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
JDK中使用DirectByteBuffer
对象来表示堆外内存,每个DirectByteBuffer
对象在初始化时,都会创建一个对用的Cleaner
对象,这个Cleaner
对象会在合适的时候执行
unsafe.freeMemory(address)
,从而回收这块堆外内存。当初始化一块堆外内存时,对象的引用关系如下:
其中first
是Cleaner
类的静态变量,Cleaner
对象在初始化时会被添加到Clener
链表中,和first
形成引用关系,ReferenceQueue
是用来保存需要回收的Cleaner
对象。
如果该DirectByteBuffer
对象在一次GC中被回收了
此时,只有Cleaner
对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次Full GC时,把该Cleaner
对象放入到ReferenceQueue
中,并触发clean
方法。
1. 堆外内存的创建
在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限,
堆外内存的限额默认与堆内内存(由-Xmx 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。
然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出OOM异常。如果额度被批准,就调用大名鼎鼎的sun.misc.Unsafe去分配内存。
2. 堆外内存基于GC的回收
存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。
快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;
当老生代也满了,就会发生full gc。这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能
在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。
这时,就只能靠前面提到的申请额度超限时触发的system.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程
睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。还有,万一,万一大家迷信某个调优指南设置了-DisableExplicitGC
禁止了system.gc(),那就不好玩了。所以,堆外内存还是自己主动点回收更好,比如Netty就是这么做的。
3. 堆外内存的主动回收
对于Sun的JDK这其实很简单,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行。
在Netty里,因为不确定跑在Sun的JDK里(比如安卓),所以多废了些功夫来确定Cleaner的存在。
4. Cleaner如何与GC相关联?
Cleaner就是PhantomReference的子类。
当GC时发现它除了PhantomReference外已不可达(持有Cleaner的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。
然后另有一条ReferenceHandler线程,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(); 如果是其他类型就放入ReferenceQueue中,
这样应用的代码可以从Queue里拖出这些理论上已死的对象,这是一种比finalizer更轻量更好的机制。
参考:
Are Java DirectByteBuffer wrappers garbage collected?
占小狼 : DirectByteBuffer
江南白衣: 堆外内存