在垃圾回收时,对于Direct Memory虚拟机虽然会执行回收,但是不受JVM直接管理,不能发现空间不足了就通知收集器回收,而是等待FullGC时,JVM会顺带清除直接内存的废弃对象。
写个代码来看直接内存的分配
public static void main(String[] args) {
while(true) {
// 每次100M
ByteBuffer.allocateDirect(100 * 1024 * 1024);
}
}
增加参数-XX:+PrintGCDetails 打印GC日志,执行程序会发现一直在执行,但是会频繁的GC/FullGC,申请的是堆外内存,堆内并不占多少空间,为什么会频繁触发GC呢?
来看DirectByteBuffer源码
内部会有一个Bits类来记录本应用已经分配了多少的直接内存空间,下边是方法中的代码,删除了很多,保留了我想说的。
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
// 获取虚拟机配置的最大允许的直接内存参数值-XX:MaxDirectMemorySize,默认和 MaxHeapSize差不多
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 记录本次分配的内存大小,检查已分配的内存是否已经超过最大允许的直接内存大小
if (tryReserveMemory(size, cap)) {
// 内存足够返回
return;
}
// trigger VM's Reference processing
// 内存不足,触发一个fullgc
System.gc();
// 检查是否进行了回收,并支持分配
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
// 不能无限次的死循环检查,设置了各最大次数
if (sleeps >= MAX_SLEEPS) {
break;
}
}
// 内存不足报错oom
throw new OutOfMemoryError("Direct buffer memory");
}
在分配直接内存超过最大限制时,会代码方式触发FullGC,所以会频繁GC且一直执行下去。
但是假若我们程序中设置了-XX:+DisableExplicitGC参数,参数表示禁用System.gc(),那么程序中的System.gc()无效,就会报错OOM。
DirectByteBuffer对象是怎么回收的呢
DirectByteBuffer buffer = new DirectByteBuffer();
当然这样子代码是不对的,我只是表达个意思。
buffer对象中保存的是堆外内存的地址,buffer这个对象还是在堆内的,这部分内存会在gc的时候检查到没有任何引用,然后垃圾回收掉,而堆外内存在buffer这个对象被回收之后,再由unsafe类回收堆外内存,下边来看具体情况。
继续来看DirectByteBuffer源码,在构造函数中初始化内存后其中有一个Cleaner对象,对象关联了当前的ByteBuffer对象和一个Runnable,Runnable中是释放内存操作,那看起来必然是在cleaner中调用的堆外内存释放
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
private final Cleaner cleaner;
DirectByteBuffer(int cap) {
// 忽略分配内存相关代码
// 初始化cleaner对象
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
public Cleaner cleaner() { return cleaner; }
}
继续跟踪Cleaner对象
public class Cleaner extends PhantomReference<Object> {
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
private final Runnable thunk;// 释放内存runnable对象
// 这个var0就是DirectByteBuffer对象, var1就是unsafe清理操作
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
}
发现Cleaner是个虚引用,关联着DirectByteBuffer,在顶层父类Reference中会发现,静态块中启动了一个ReferenceHandler 线程死循环执行tryHandlePending方法,而方法内部的pending变量是个链表,gc时会将引用对象不可达的引用对象放入pending链表(这一步我看别的博客说的),在本例中,即DirectByteBuffer大对象被回收时,gc会将虚引用Cleaner对象放入pending链表,线程运行时,判断pending中的对象是不是一个Cleaner,如果是调用其clean()方法,本例中即unsafe释放内存操作。
public abstract class Reference<T> {
private static Reference<Object> pending = null;
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
private static class ReferenceHandler extends Thread {
private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}
static {
// pre-load and initialize InterruptedException and Cleaner classes
// so that we don't get into trouble later in the run loop if there's
// memory shortage while loading/initializing them lazily.
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
c = r instanceof Cleaner ? (Cleaner) r : null;
pending = r.discovered;
r.discovered = null;
} else {
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
}
这个Reference类是弱引用WeakReference、软引用SoftReference、虚引用PhantomReference的共同父类,三种引用中的构造方法中都会有一个ReferenceQueue,在Reference的tryHandlePending方法最后也可以看到若当前pending中的Reference引用对象构造时的队列不为null,会将当前引用对象入队。程序中可以关注这个队列来获取一个通知,即这个Reference对象所关联的对象被回收的通知