JDK的堆外内存申请
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024)
跟踪源码这里的ByteBuffer的实现类实际上是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内部维护了
//已申请内存大小totalCapacity
//最大内存大小maxMemory,该值可以通过-XX:MaxDirectMemorySize设置
//Bits的作用就是在真正申请内存之前做验证,如果内存空间不够提前抛出异常,当然在抛异常之前也会尝试做一些挣扎
//例如主动调用System.gc()等,看能否释放一些已有的堆外内存
Bits.reserveMemory(size, cap);
long base = 0;
try {
//真正的申请操作在这里,是个native方法
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类继承了PhantomReference 也就是幽灵引用
//当DirectByteBuffer被GC的时候 cleaner会触发内存回收的动作
//这里的base就是本次申请的内存地址,释放的时候需要
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
以上是JDK的堆外内存申请过程, 通过Bits对申请做验证以及存储大小的统计,调用unsafe.allocateMemory申请堆外内存,最后 再将自己注入到Cleaner中,在gc之后通过Cleaner完成后续的内存释放
JDK的堆外内存释放
先看Cleaner类被DirectByteBuffer调用的部分
//Deallocator就是 DirectByteBuffer的内部类,真正释放内存的操作就由这个类完成
public static Cleaner create(DirectByteBuffer var0, Deallocator var1) {
//调用add方法,是为了将自己被一个静态变量引用,防止被GC
return var1 == null ? null : add(new Cleaner(var0, var1));
}
Cleaner的构造方法
private Cleaner(DirectByteBuffer var1, Deallocator var2) {
//调用Reference的构造方法, 接下来就由Reference来控制Cleaner
super(var1, dummyQueue);
this.thunk = var2;
}
接下来继续看Reference,首先关注的是 static代码块
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
//重点在这里,这里开启了一个线程,实际上就是运行一个循环,不停的调用tryHandlePending(true)
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();
//这里主要是为了Bits类在挣扎的时候能够主动调用tryHandlePending方法,看看能否释放一些内存空间
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
接下来就重点看看tryHandlePending的方法,这里我把jdk的注释也拿了过来
/* Object used to synchronize with the garbage collector. The collector
* must acquire this lock at the beginning of each collection cycle. It is
* therefore critical that any code holding this lock complete as quickly
* as possible, allocate no new objects, and avoid calling user code.
*/
//通过注释可以了解到,这个lock是垃圾收集器和code代码共用的
static private class Lock { }
private static Lock lock = new Lock();
/* List of References waiting to be enqueued. The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object. The
* list uses the discovered field to link its elements.
*/
//这里是重点,垃圾收集器在对象被回收后 会将相应的Reference放到这里,例如前面提到的Cleaner
private static Reference<Object> pending = null;
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
//对Cleaner进行特殊处理 调用clean 执行内存释放的操作
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
//相应的例如 WeakReference、softReference 等都是在这个时候被放入队列的
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
接下来我们主要看Cleaner.clean()的逻辑
public void clean() {
if (remove(this)) {
try {
//这里的thunk就是之前传入进来的Deallocator,这里开始调用它的run方法
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;
}
});
}
}
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
//真正的内存释放
unsafe.freeMemory(address);
address = 0;
//再次调用Bits 修改相应的统计数据
Bits.unreserveMemory(size, capacity);
}
以上就是jdk的堆外内存申请以及释放, 整体来说并不那么可靠,因为java的gc我们是没办法准确控制的,一个不小心就有可能导致内存泄漏, 而且申请内存的过程需要额外做不少操作,性能难免也有影响
netty的堆外内存申请
为了更好的比较这里只讲 UnpooledByteBufAllocator
UnpooledByteBufAllocator.DEFAULT.buffer(1024)
追踪源码得到
//io.netty.buffer.UnpooledByteBufAllocator#newDirectBuffer
//这里根据是否能获取 sun.misc.Unsafe 和 noCleaner参数来决定使用哪个类
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
final ByteBuf buf;
if (PlatformDependent.hasUnsafe()) {
buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
}
分别是
InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf
InstrumentedUnpooledUnsafeDirectByteBuf
其中 InstrumentedUnpooledUnsafeDirectByteBuf就和jdk的申请套路是一样的
这里主要分析 InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf 这也是大部分情况下的选择
继续跟踪代码
//io.netty.buffer.UnpooledDirectByteBuf#UnpooledDirectByteBuf(io.netty.buffer.ByteBufAllocator, int, int)
public UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
super(maxCapacity);
if (alloc == null) {
throw new NullPointerException("alloc");
}
checkPositiveOrZero(initialCapacity, "initialCapacity");
checkPositiveOrZero(maxCapacity, "maxCapacity");
if (initialCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format(
"initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
}
this.alloc = alloc;
//重点在allocateDirect方法上
setByteBuffer(allocateDirect(initialCapacity), false);
}
//io.netty.buffer.UnpooledByteBufAllocator.InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf#allocateDirect
protected ByteBuffer allocateDirect(int initialCapacity) {
ByteBuffer buffer =
//调用父类io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf的方法
super.allocateDirect(initialCapacity);
((UnpooledByteBufAllocator) alloc()).incrementDirect(buffer.capacity());
return buffer;
}
protected ByteBuffer allocateDirect(int initialCapacity) {
//找到最终的入口
return PlatformDependent.allocateDirectNoCleaner(initialCapacity);
}
接着继续看PlatformDependent类
static ByteBuffer allocateDirectNoCleaner(int capacity) {
// Calling malloc with capacity of 0 may return a null ptr or a memory address that can be used.
// Just use 1 to make it safe to use in all cases:
// See: http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html
return newDirectBuffer(UNSAFE.allocateMemory(Math.max(1, capacity)), capacity);
}
看到这里想必大家明白为什么取名叫NoCleaner了
这里的申请直接调用了native方法 也就是 UNSAFE.allocateMemory,没有jdk繁琐的过程,也没有创建相应的cleaner,换句话说该内存的释放只能手动来操作了
接下来的操作就是创建DirectByteBuffer了
static ByteBuffer newDirectBuffer(long address, int capacity) {
ObjectUtil.checkPositiveOrZero(capacity, "capacity");
try {
//因为DirectByteBuffer类 不是public 所以只能通过反射来创建,然后传入申请的内存地址
return (ByteBuffer) DIRECT_BUFFER_CONSTRUCTOR.newInstance(address, capacity);
} catch (Throwable cause) {
// Not expected to ever throw!
if (cause instanceof Error) {
throw (Error) cause;
}
throw new Error(cause);
}
}
netty的堆外内存释放
这里从release()方法入手
ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer(1024);
byteBuf1.release();
//io.netty.buffer.AbstractReferenceCountedByteBuf#release()
public boolean release() {
//这里的原理是对ByteBuf的引用做个计数,release表示引用减1,当引用为0时就可以进行释放
return handleRelease(updater.release(this));
}
继续追踪代码
//io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf#freeDirect
protected void freeDirect(ByteBuffer buffer) {
PlatformDependent.freeDirectNoCleaner(buffer);
}
PlatformDependent这个类是不是很熟悉,申请内存也是这个类
public static void freeDirectNoCleaner(ByteBuffer buffer) {
assert USE_DIRECT_BUFFER_NO_CLEANER;
int capacity = buffer.capacity();
//这里PlatformDependent0.directBufferAddress(buffer)获取的就是buffer对应的堆外内存地址了
//然后调用UNSAFE.freeMemory(address) 进行释放
PlatformDependent0.freeMemory(PlatformDependent0.directBufferAddress(buffer));
decrementMemoryCounter(capacity);
}
对比明显发现netty的代码更加的简洁,虽然不能通过gc来自动释放内存,但是针对于这种特殊需求,手动控制反而更加合理
除此之外netty还包括内存的度量工具类 例如UnpooledByteBufAllocatorMetric、PooledByteBufAllocatorMetric
为了防止忘记释放内存,还有对应的检测工具 ResourceLeakDetector