1.使用场景
Netty:
- Unpooled.directBuffer()执行时,会以256字节(AbstractByteBufAllocator.DEFAULT_INITIAL_CAPACITY)调用nio的
ByteBuffer.allocateDirect(int capacity)
方法。 - ByteBuffer.allocateDirect中创建了
DirectByteBuffer
对象.
2.DirectByteBuffer
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
// 是否需要页对齐
boolean pa = VM.isDirectMemoryPageAligned();
// 每个页的大小
int ps = Bits.pageSize();
// 如果需要对齐,则多申请一页。最小1字节
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 1.检查堆外内存,并记录直接内存
Bits.reserveMemory(size, cap);
// 内存基址
long base = 0;
try {
// 2.按照size字节开辟内存空间,并返回内存基址。
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 3.内存区域初始化为0
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;
}
// 4.创建Cleaner
/* Cleaner是一个PhantomReference(虚引用)
Cleaner构造函数接受一个Runnable参数,在执行clean()的时候做清理
这里传入了一个Deallocator的Runnable对象,执行unsafe.freeMemory清理内存
*/
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
3.Bits.reserveMemory
- 调用的
tryReserveMemory
方法,尝试计算可以使用的直接内存。 - 默认的最大直接内存:
sun.misc.VM
.directMemory = 67108864L,即64M。 tryHandlePendingReference
调用sun.misc.Cleaner.clean()
方法清理内存,最终是在Deallocator.run中调用unsafe.freeMemory
释放内存。- 如果启动时加上
-XX:+DisableExplicitGC
,则System.gc()
不会起作用。所以用到堆外内存的应用,不应该加此参数。
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
/*
获取最大直接内存
如果没有通过-XX:MaxDirectMemorySize指定,则使用67108864字节,即64M作为最大直接内存。
*/
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 1. 尝试计算内存是否充足
if (tryReserveMemory(size, cap)) {
return;
}
/*
在Reference的静态代码中:
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
*/
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// 2. 调用所有Cleaner.clean()方法
while (jlra.tryHandlePendingReference()) {
// 尝试计算内存是否充足
if (tryReserveMemory(size, cap)) {
return;
}
}
// 3. 触发gc
System.gc();
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
// 4. 尝试计算内存是否充足
if (tryReserveMemory(size, cap)) {
return;
}
// MAX_SLEEPS = 9
if (sleeps >= MAX_SLEEPS) {
break;
}
// 5. 调用所有Cleaner.clean()方法
if (!jlra.tryHandlePendingReference()) {
try {
// 9次循环,sleep时间分别为 1, 2, 4, 8, 16, 32, 64, 128, 256毫秒。
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// 抛出OOM
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
4.Cleaner
sun.misc.Cleaner
是PhantomReference
的子类,是一个链表
- 三个属性:
// 下一个元素
private Cleaner next = null;
// 上一个元素
private Cleaner prev = null;
// clean()时执行的任务
private final Runnable thunk;
- 两个静态变量:
// 引用队列,在执行静态方法create的时候,用来设置Reference.queue。
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
// 链表的头元素,在clean()中移除当前元素时,会从这里开始找。
private static Cleaner first = null;
- 创建:
// 私有构造函数创建对象,并且加到静态变量first的队尾。
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
- clean方法:
public void clean() {
if (remove(this)) { // 从first中移除当前对象
try {
// 执行清理任务,即DirectByteBuffer中创建的Deallocator对象
this.thunk.run();
} catch (final Throwable var2) {
....
}
}
}
- 在哪里调用?在Reference.tryHandlePending中。
5.Reference.tryHandlePending
tryHandlePending
是Reference中一个核心的静态方法,用来处理pending状态的引用:
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 当前Reference对象是不是Cleaner对象。
c = r instanceof Cleaner ? (Cleaner) r : null;
// 省略代码
} else {
// 省略代码
}
}
} catch (OutOfMemoryError x) {
// 省略代码
}
if (c != null) {
// 执行Cleaner.clean()
c.clean();
return true;
}
// 省略代码
return true;
}
有两个地方调用:
- 1 Reference的静态代码块中启动的线程
ReferenceHandler
中:
private static class ReferenceHandler extends Thread {
// 省略代码
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
// Reference的静态代码块
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 省略代码
}
- 2 Reference的静态代码块中提供的JavaLangRefAccess的实现中;
static {
// 省略代码
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
这个实现会被其他类调用,比如Bits.reserveMemory中的
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
。
6.Deallocator
- 1 实现了Runnable
- 2 在run中通过unsafe.freeMemory等释放内存。
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);
}
}
总结
- 1 通过unsafe.allocateMemory和unsafe.setMemory开辟堆外内存空间并初始化;
- 2 通过unsafe.freeMemory回收内存;
- 3 通过Bits.reserveMemory尝试保留内存空间,不足时通过Reference.tryHandlePending,System.gc()等方式尝试释放内存;
- 4 使用Cleaner,在DirectByteBuffer对象回收时,进行堆外内存的回收(Deallocator);
- 5 尝试分配堆外内存时,Bits.reserveMemory会调用System.gc(),所以在有堆外内存的场景下,不建议添加-XX:-+DisableExplicitGC,否则System.gc()会失效。