以下是对以前资料的整理,汇总如下!
狭义上来讲,堆外内存通常指的就是DirectByteBuffer
为什么要使用堆外内存
ByteBuffer有2个实现,DirectByteBuffer
和HeapByteBuffer
。
DirectByteBuffer的缺点是分配是比较耗时,所以通常都是开局申请一堆,然后自己进行管理。【不理解】
DirectByteBuffer还有一个缺点就是GC的冰山对象问题。
首先DirectBytBuffer被GC的时候,会调用cleaner进行堆外的内存清理
但是因为DirectByteBuffer只是一个引用,撑过了几轮YoungGC后,就会进入Old区,那里可不方便进行GC
那么堆外的一堆内存就没有办法清理的吗?其实不会的,分配的时候还有Bits
起到宏观调控的作用。
Bits
类申请,该类会通过MaxDirectMemorySize
进行判定是否需要进行后续操作System.gc
进行一次FullGC
,所以千万不要禁用:-XX:+DisableExplicitGC
,100ms后再次判定,如果还不够那么就会OOM-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/gc
是没作用的,因为这是堆外,不受GC管理。假如分配一个堆外内存,通常采用
ByteBuffer.allocateDirect(1024)
我们看下他内部代码,代码有格式调整
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
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;
}
DirectByteBuffer
的构造函数Bits
是JDK对于堆外内存使用量的一个全局管理器,能不能继续分配,需不需要GC都需要他作为调控UNSAFE.allocateMemory
是具体分配对外内存的,本质还是JNI来分配cleaner
是为了正常GC的时候,将堆外内存给清理掉Deallocator
是一个DirectByteBuffer
的内部私有类,同时实现了Runable
的类,其中的run
方法实现如下
private static class Deallocator
implements Runnable {
public void run() {
if (address == 0) {
// Paranoia
return;
}
UNSAFE.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
核心就是调用:Unsafe.freeMemory
,这个方法内部调用的是一个native
方法freeMemory0(address);
这个类是在DirectByteBuffer
构造的时候,创建的
DirectByteBuffer(int cap) {
// ...
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}
创建了一个类的成员变量cleaner
,这个类就负责销毁对外内存,那么他是如何实现内存销毁的了?
public class Cleaner
extends PhantomReference<Object>{
....
}
最核心的就是Cleaner
继承了PhantomReference
,每次GC的时候,会清理DirectByteBuffer
指向堆外的真实内存。
这个调用链其实就比较清晰了:
ByteBuffer
对象被GCcleaner
是其成员变量,所以会被GCDeallocator
的UNSAFE.freeMemory(address)
Cleaner 类继承了 PhantomReference 类,并且在自己的 clean() 方法中启动了清理线程,当 DirectByteBuffer 被 GC 之前 cleaner 对象会被放入一个引用队列(ReferenceQueue),JVM 会启动一个低优先级线程扫描这个队列,并且执行 Cleaner 的 clean 方法来做清理工作。
这个类在哪些地方被调用了?
打印堆外对象的信息
@Scheduled(initialDelay = 3000, fixedDelay = 4000)
public void direct66() throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = new ObjectName("java.nio:type=BufferPool,name=direct");
MBeanInfo info = mbs.getMBeanInfo(objectName);
for (MBeanAttributeInfo i : info.getAttributes()) {
System.out.println(i.getName() + ":" + mbs.getAttribute(objectName, i.getName()));
}
}