Direct Buffer介绍
Direct Buffer
前言
上篇文章Buffer末尾中谈到堆内Buffer(Heap Buffer)和直接Buffer(Direct Buffer)的概念,但是却一笔带过,并未涉及其细节,这篇文章继续聊聊Buffer——Direct Buffer。
- Direct Buffer是什么
- Direct Buffer和Heap Buffer的区别
- 用来干什么
- Direct Buffer和JVM
另外需要说明的是,为了叙述的方便,和上篇Buffe文章类似,本文中以DirectByteBuffer为例介绍。
二.是什么?
Direct vs. non-direct buffers
A direct byte buffer may be created by invoking the allocateDirect factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance.
以上内容引用ByteBuffer的java api描述,可移步ByteBuffer,这里先根据以上内容总结下直接内存是个啥。
说到底,DirectByteBuffer引用空间就是一块内存,不过该块内存通常被分配在jvm的堆区域外,即该块内存不在堆区域内,常称作:堆外内存,下面以堆外内存叙述。
还可以参考R大(RednaxelaFX)的回复Java NIO中,关于DirectBuffer,HeapBuffer的疑问?
三.区别
上面说到堆外内存是什么,那么它和堆内内存有啥区别?
-
堆内内存,jvm运行时区域的一部分,即堆区,分为年轻代、老年代。该块的内存管理委托给jvm。
-
堆外内存,堆内内存以外的都可以称为堆外内存。关于堆外内存的理解,可以参考笨神(你假笨)的一篇文章JVM源码分析之堆外内存完全解读。
如下图,描述了它们之间的关系:
jvm说到底也是运行在操作系统上的应用程序,java应用程序运行在jvm上时,jvm为其分配相应的内存空间,包括:程序计数器、堆、栈等等。java的对象都是在堆区的,所以DirectByteBuffer作为java对象,它自然也是在堆区域,只是DirectByteBuffer对象表示的存储空间是放在堆区域外部的。
下面再来看下java的具体实现:
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
address长整型变量是Buffer类的属性域,只仅仅被堆外内存使用。该address指向的java堆区域以外的一段内存空间的起始地址。每个DirectByteBuffer持有一个这样的address变量,即可以快速的寻址。
- 堆外内存的分配回收代价要高于堆内内存
- 通过
ByteBuffer.allocateDirect(int capacity)
堆外堆内创建,创建java对象时即创建了堆内内存 - 堆外内存可以用于操作系统的本地io操作,而堆内内存由于gc的缘故暂不支持,具体细节可以参考上面R大对于"pin"的说法
三.应用场景
- A direct byte buffer may also be created by mapping a region of a file directly into memory.
- Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.
- It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations.
Java中非常典型的两种用法:
-
作为内存文件映射,直接将文件中区域的内容映射至内存,java中的代表类
MappedByteBuffer
的实现类就是使用DirectByteBuffer(关于MappedByteBuffer的原理可以移步至 ——> 狼哥文章:深入浅出MappedByteBuffer 和 NIO包-Buffer类:内存映射文件DirectByteBuffer与MappedByteBuffer(二)如果想更深入的理解MappedByteBuffer的原理,推荐了解zero-copy机制,可移步至通过零拷贝实现有效数据传输 和 零复制(zero copy)技术
-
操作系统本地I/O直接存取数据至堆外内存,避免拷贝中间缓冲区。Java中Channel的read/write都是用到了DirecByteBuffer作为本地I/O操作
由于是堆外内存,不属于jvm运行区域,减少了gc影响,所以可用于本地I/O操作,参考R大的“pin”观念。
四.分配与回收
虽然是堆外内存,也是有分配和回收策略的,接下来简单的了解下堆外内存的分配与回收策略。
在以前学习c语言的时候,学习过其两个关于内存分配与回收的函数:malloc和free,分别用来申请分配内存/回收释放内存。
由于java的内存管理是委托给jvm处理的,所以基本上java应用开发者不需要要太关系内存这块的处理,那么堆外内存又是如何分配与回收呢。
java的中提供的unsafe
类,相信读者们都不陌生。java提供出来的不安全操作的封装类,该类提供的操作在java中认为是不安全的,比如内存的直接分配与回收,有违内存自动管理的机制,所以这个类的访问控制只能由rt.jar访问。堆外内存的分配与回收正是使用了
public native long allocateMemory(long var1);
public native void freeMemory(long var1);
它的以上两个api直接完成分配与回收,由于是jni,底层基于malloc和和free实现。
作为java应用的开发者,我们一般通过Buffer实现类的的静态方法
ByteBuffer.allocatDirect(int capacity)
分配指定大小的堆外内存空间。
但是堆外内存肯定不是无限分配,这样容易引起内存溢出,系统异常。java中堆外内存大小可以通过jvm参数-XX:MaxDirectMemorySize指定,如果该参数未被指定则使用-Xmx参数指定的大小堆大小作为堆外内存大小。
下面分析下源码的方式来更深入的理解下堆外内存的分配过程。allocatDirect的方法如下
/**
* Allocates a new direct byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. Whether or not it has a
* {@link #hasArray backing array} is unspecified.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
从java doc中知道,分配的buffer的position为0(如果对Buffer还不甚了解,可以移步至我的上篇关于《Buffer》博文),limit为capacity,mark未被定义,buffer中存储元素被初始为0。
下面更进一步看下DirectByteBuffer的构造方法
// Primary constructor
//
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;
}
从以上源码可以看出,首先构造方法访问控制为包权限,这样可以避免让应用代码入侵。又不失jdk可以进行自由实现。
整个初始过程可以分为以下几个过程:
- 调用父类构造方法进行初始化处理
- 获取是为页对齐并获取页大小
- 分配大小至少为1byte。如果分配容量小于1byte,则分配大小设置为1byte。如果是页对齐分配,则设置容量为大于cap的最小的页大小整数倍。否则设置容量为cap
- 进行内存预留操作(主要是为了锁定内存,用于后续的内存分配,其中使用cas操作)
- 使用unsafe分配内存
- 设置分配的内存的起始地址address(该变量被Buffer class对象持有),如果不是整页地址,则进行滚动到页边界
- 设置该块堆外内存清理器Cleaner(用于后续的内存回收释放)
经过以上过程,完成了堆外内存的分配。
关于回收的详细过程,建议移步至上面提及的笨神的文章,这里只做简单的描述。
回收使用unsafe.freeMemory
既然要回收堆外内存,那必须知道该块内存的的地址,与之唯一相关联的DirectByteBuffer对象,该对象中存有内存的地址address。接下来就要知道什么时候回收,这个就和jvm gc息息相关了,如何决策堆外内存回收呢?显然是该块内存不使用时,该块内存是否使用可以通过DirectByteBuffer对象是否要被gc回收。
就这样,知道回收哪里,什么时间点回收,就不难处理了。
引用笨神的描述:
DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块
上面的堆外内存创建过程中,描述到设置清理器Cleaner,该类就是PhantomReference的实现。
再可以看下ReferenceHandler的处理
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);
}
}
}
其中调用tryHandlePending
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
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
最后调用c.clean(),即调用了DirectByteBuffer中的cleaner。
再来看下Cleaner.clean
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;
}
});
}
}
}
这里调用了thunk.run,再回头看下DirectByteBuffer的初始过程中,Cleaner的初始化调用了Cleaner的工厂方法create,对于传入的Runnable实现是Deallocator
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);
}
}
从DirectByteBuffer和Deallocator的构造方法中,不难看出,Deallocator.address就是堆外内存的地址,在run方法中调用了unsafe.freeMemory(address);
回收释放堆外内存。
通过以上的两个过程大致介绍,也算是对堆外内存的分配与回收有个宏观的了解。由于个人技术能力有限,如果有错误的地方,请读者不吝给出宝贵意见。