关于MaxDirectMemorySize的设置
2017-06-07 本文已影响1402人
三斤牛肉
最近在查一个堆外内存泄露的问题,顺便学习了下MaxDirectMemorySize使用。
总所周知-XX:MaxDirectMemorySize可以设置java堆外内存的峰值,但是具体是在哪里限制的呢,来跟踪下创建DirectByteBuffer的过程。
找到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;
}
主要代码:
- Bits.reserveMemory(size, cap);检测是否足够分配空间,并增加内部总量计数器
- unsafe.allocateMemory(size); 通过Unsafe类分配内存地址
- cleaner = Cleaner.create(this, new Deallocator(base, size, cap));创建清理类,用于内存回收
java.nio.Bits
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, int cap) {
//因为内存分配是全局的,所以必须加锁
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory(); //最大堆外内存设置
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
//如果剩余空间足够,增加总量计数器直接返回
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return;
}
}
//如果剩余空间不足,那么先执行一次GC
System.gc();
try {
Thread.sleep(100);//其实jvm也用了很low的sleep下,等待GC完成
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
//这里同样,在计算的时候必须是同步的
synchronized (Bits.class) {
//如果内存空间还是不够,则抛出异常
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}
在GC的时候totalCapacity会被释放,看下具体实现。
在DirectByteBuffer中的内部类Deallocator:
...省略
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);//释放内存
address = 0;
Bits.unreserveMemory(size, capacity);//回收totalCapacity
}
...省略
java.nio.Bits:
//减少使用内存总量的计数器
static synchronized void unreserveMemory(long size, int cap) {
if (reservedMemory > 0) {
reservedMemory -= size;
totalCapacity -= cap;
count--;
assert (reservedMemory > -1);
}
}
Deallocator在DirectByteBuffer的构造函数中:
DirectByteBuffer(int cap) { // package-private
...省略
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
...省略
}
我们来仔细看下Cleaner对象:
...省略部分代码
public class Cleaner extends PhantomReference<Object> {
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null?null:add(new Cleaner(var0, var1));
}
private static synchronized Cleaner add(Cleaner var0) {
if(first != null) {
var0.next = first;
first.prev = var0;
}
first = var0;
return var0;
}
public void clean() {
if(remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
if(System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
}
Cleaner继承了虚引用,并且内部是一个双向链表,每次create时添加到表头。
虚引用简单来讲就是在GC时,将对象放入ReferenceQueue中,具体的可以搜索相关知识,再看执行Deallocator线程的clean函数,找到调用的地方:
public abstract class Reference<T> {
...省略部分代码
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
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();
}
}
ReferenceHandler是全局的守护线程,用于将虚引用的对象添加到ReferenceQueue中
看到在ReferenceHandler中enqueue前,会先检测Cleaner对象,并且这里continue了,也就是说Cleaner对象不会像普通的虚引用一样放入ReferenceQueue中。
所以可以理解为Cleaner使用虚引用为的是利用ReferenceHandler线程在gc时释放堆外内存。