随笔分类
对 DirectByteBuffer解读
浅谈
NIO离不开 DirectByteBuffer,若能在一开始就去合理地去使用 DirectByteBuffer,能够减少一次用户态与内核态数据之间的拷贝,这一点想必是耳熟能详的;但 DirectByteBuffer本身是堆外内存,其不受 gc所管控,但堆外内存也是需要被回收的,否则就会造成 内存泄漏,那 jvm是如何来处理这个问题的呢?
先来看个 demo:TestPhantomReference
public class TestPhantomReference {
public static void main(String[] args) {
// 创建引用队列
ReferenceQueue queue = new ReferenceQueue();
byte[] buf = new byte[1024 * 1024 * 10];
// 传入 ReferenceQueue
PhantomReference phantomReference = new PhantomReference(buf, queue);
// 置空强引用
buf = null;
System.out.println("执行gc前, queue中是否有数据?" + " " + (queue.poll() == null ? "没有" : "有"));
System.out.println("执行gc前, ref中引用对象: " + phantomReference.get());
// 发生 gc后, buf对应的对内存会被回收 - JVM本身是认识 Reference的
// 在进行可达性分析时, 若是有 Reference引用间接指向某块堆内存, JVM会将其忽略, 即 Reference去引用其他堆内存不算是强引用
// 在发生 gc时, 该被回收的话还是会被回收的
System.gc();
System.out.println("执行gc后, ref中引用对象: " + phantomReference.get()); // 对于 PhantomReference.get(), 始终返回的会是 null
System.out.println("queue中获取的 ref和 weakRef是否一致: " + (queue.poll() == phantomReference ? true : false));
}
}
运行结果:
执行gc前, queue中是否有数据? 没有
执行gc前, ref中引用对象: null
执行gc后, ref中引用对象: null
queue中获取的 ref和 weakRef是否一致: true
从执行结果上看,不难分析出 ReferenceQueue的作用:当 Reference中关联对象被回收时, Reference实例对应引用会被加入到引用队列中去
因此,我们可以通过引用队列来去检查 Reference关联的对象是否有被 gc,当 Reference关联对象被 gc时, Reference引用会被加入到引用队列中去,基于此, 当我们发现引用队列中有数据时, 便可以去获取 Reference去做些其它事情了
对构造的解读
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 - 保存创建的堆外内存对应的虚拟内存地址
// allocateMemory 本地方法, 实际上触发系统调用去分配内存
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;
}
// 内存释放器
// 参数 1: DirectByteBuffer实例
// 参数 2: Deallocator实例
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
构造中使用 Unsafe去分配了堆外内存,Unsafe本身也是可以去释放堆外内存的
最终都会对应去调用 native方法
private native long allocateMemory0(long bytes);
private native void freeMemory0(long address);
在构造中为 Cleaner分配了内存,进行相关初始化
创建 Cleaner时,传递了参数:this (DirectByteBuffer)、Deallocator实例自身 - 可以理解为 Deallocator是内存释放器
// Deallocator - 内存释放器
private static class Deallocator
implements Runnable
{
Deallocator实现了 Runnable,对应其 run:
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 通过 Unsafe调用本地方法 freeMemory去释放堆外内存
UNSAFE.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
可以看出,在 run()中通过调用 Unsafe.freeMemory()去释放了堆外内存
不难猜想,DirectByteBuffer对应的堆外内存的释放就是通过 Deallocator来实现的,这其中的逻辑又是怎么整合起来的呢?
继续看:
Cleaner是 DirectByteBuffer的一个字段,实现了虚引用
// Cleaner 继承了虚引用
private final Cleaner cleaner;
// 继承了虚引用 - PhantomReference
public class Cleaner
extends PhantomReference<Object>
{
// Cleaner自己来创建了 引用队列
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
Cleaner构造
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
// 将 Deallocator实例引用保存到了 thunk中去
this.thunk = var2;
}
将 Deallocator实例引用保存到了 thunk中去
在创建 cleaner时,传递了 this和 Deallocator实例,而 DirectByteBuffer在被回收后 (即 Ref关联对象),cleaner会被加入到队列中去,一开始是 Reference.pending队列中去
光从构造中似乎看不出,但可以猜出其实现:依靠于 Reference!
对 Reference解读
Ref状态变迁如下:
字段
// 存放真实对象引用
private T referent; /* Treated specially by GC */
// 引用队列, 外部可以通过传递引用队列, 方便后续判断 Ref关联对象 referent是否有被 gc回收掉
volatile ReferenceQueue<? super T> queue;
// 保存引用队列中其下一个元素的引用 - next构造 RefQueue单向链表
@SuppressWarnings("rawtypes")
volatile Reference next;
// vm线程在判定当前 ref关联 obj是垃圾后, 会将当前 ref加入到 pending队列、
// pending队列是一个单向链表, 使用 discovered 连接起来
private transient Reference<T> discovered;
// pending 链表的头部字段
// pending链表头部的追加字段, 是由 jvm垃圾收集器线程进行追加的
private static Reference<Object> pending = null;
构造
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
初始化
在加载 Reference时,会去执行一块静态代码:
// 加载 Reference时执行
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 创建了 ReferenceHandler
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 waitForReferenceProcessing()
throws InterruptedException
{
return Reference.waitForReferenceProcessing();
}
@Override
public void runFinalization() {
Finalizer.runFinalization();
}
});
}
在初始化代码块中,创建了 ReferenceHandler,并去执行了其 start()
ReferenceHandler是什么?
private static class ReferenceHandler extends Thread {
// 加载
private static void ensureClassInitialized(Class<?> clazz) {
try {
// 这里来保证 Cleaner类已经被 jvm加载过了的
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}
// 初始化时执行
static {
// pre-load and initialize Cleaner class so that we don't
// get into trouble later in the run loop if there's
// memory shortage while loading/initializing it lazily.
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, null, name, 0, false);
}
public void run() {
while (true) {
// 消费 Pending队列中元素
processPendingReferences();
}
}
}
初始化 Reference时,当前线程 (ReferenceHandler)会去消费 pending队列中元素
static boolean tryHandlePending(boolean waitForNotify) {
// 保存当前线程想要去消费 Ref的引用
Reference<Object> r;
// 关键
Cleaner c;
try {
// 这里为什么需要同步 ?
// 1.jvm垃圾收集器线程需要向 pending队列追加 ref
// 2.当前线程消费 pending队列
synchronized (lock) {
if (pending != null) {
// 获取 pending队列头元素
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
// c 一般情况下是 null, 当 r指向的 ref实例时 cleaner实例时, c才会不为 null, 并且去指向 cleaner对象
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) {
// 1.释放锁
// 2.阻塞当前线程, 直到其它线程调用了当前 lock.notify() | lock.notifyAll().
lock.wait();
// 唤醒当前消费线程是谁 ? - jvm垃圾回收线程, 添加 ref到 pending后, 会去调用 lock.notify()
}
// 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
// true - 当前 ref是 clean类型实例, 就不会去执行 ref的入队逻辑了.
if (c != null) {
c.clean(); // 直接来执行了 Cleaner.clean()。
return true;
}
// 获取 ref中关联的引用队列
ReferenceQueue<? super Object> q = r.queue;
// true - 创建 ref时指定了 refQueue - 执行 queue入队逻辑
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
可以看出,当线程在消费 pending队列中元素 (Ref引用)时,若元素不是 Cleaner实例,且其关联了 ReferenceQueue,会被加入到 ReferenceQueue中去,并设置了下状态;当元素是 Cleaner实例时,直接就去执行了 Cleaner.clean()
public void clean() {
if (!remove(this))
return;
try {
// 这里来调用了 Deallocator.run() - 实现了对堆外内存的回收
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}
可以看到,clean()中调用了 thunk.run(),而 thunk便是创建 cleaner时传递进去的 Deallocator
这样就实现了堆外内存的释放:DirectByteBuffer存在字段 Cleaner,当 DirectByteBuffer被 gc时,jvm垃圾回收线程会将 Cleaner引用加入到 pendding队列中去,当线程 (ReferenceHanlder)去消费 pendding中元素时,检测发现其是 Cleaner类实例,此时不会去将该引用添加到 RefQueue中,而是直接去执行了 Cleaner.clean(),在里头调用了 this.chunk.run(),对应的便是 Deallocator.run(),在这里头去释放了堆外内存!
至此,DirectByteBuffer内存释放解读完毕!