1 堆外内存
JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,不如Netty,广泛使用了堆外内存,但是这部分内存不归JVM管理,GC算法并不会对它们进行回收,所以使用堆外内存是需要格外小心,以防出现内存泄露。
2 堆外内存的申请和释放
JDK中使用DirectByteBuffer对象来表示堆外内存,可以通过-XX:MaxDirectMemorySize来指定最大的堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,在Cleaner对象回收的时候回收这部分堆外内存。初始化时引用关系如下:
其中first是Cleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。
3 Cleaner如何与GC相关联
JDK除了StrongReference、SoftReference和WeakReference之外,还有一种PhantomReference是虚引用,Cleaner就是PhantomReference的子类。(针对这几种引用,后续专题讲解)
当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。
4 堆外内存基于GC的回收
快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。
这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。
其实在初始化DirectByteBuffer对象时,如果当前堆外内存的条件很苛刻时,会主动调用System.gc()强制执行FGC。
这时,就只能靠触发system.gc()来救场了。如果还是无法释放,就可能会出现OOM。
不过很多线上环境的JVM参数有-XX:+DisableExplicitGC,导致了System.gc()等于一个空函数,根本不会触发FGC,这点需要特别关注。