buffer一般是用于字节缓存的,先buffer一段,然后再flush出去。一般在网络交互中用的比较多,比如java的nio框架,如netty等。
HeapByteBuffer与DirectByteBuffer,在原理上,前者可以看出分配的buffer是在heap区域的,其实真正flush到远程的时候会先拷贝得到直接内存,再做下一步操作 (考虑细节还会到OS级别的内核区直接内存),其实发送静态文件最快速的方法是通过OS级别的send_file,只会经过OS一个内核拷贝,而不会来回拷贝;在NIO的框架下,很多框架会采用 DirectByteBuffer来操作,这样分配的内存不再是在java heap上,而是在C heap上,经过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比HeapByteBuffer 要快速好几倍。
ByteBuffer.allocate(int capacity);参数大小为字节的数量
这种方式内存是在Java Heap中分配的。
ByteBuffer.allocateDirect(int capacity);
堆外内部最终分配内存是通过 Unsafe.allocateMemory() 来实现的,不是在Java Heap中分配的。这个unsafe默认情况下java代码是没有能力可以调用到的,不过你可以通过反射的手段得到实例进而做操作, 当然你需要保证的是程序的稳定性,既然叫unsafe的,就是告诉你这不是安全的,其实并不是不安全,而是交给程序员来操作,它可能会因为程序员的能力而导致不安全,而并非它本身不安全。java9好像移除了Sun.misc.Unsafe API。
普遍的说法是堆外内存会快一些,原因主要有:
堆外最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放,可以使用 -XX:MaxDirectMemorySize 参数指定堆外内存最大大小。
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));
//在这个方法内会校验是否指定了最大堆外内存,以及是否有足够可用的堆外内存空间,如果不足会进行full gc
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 = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
初始化时会创建cleaner,这个cleaner是一个PhantomReference类型的,会在System.gc()时进行回收,这个cleaner内部提供有供堆外内存回收的clean方法,通过这个方法可以手动进行堆外内存回收,是堆外内存回收的关键。
Bits.reserveMemory方法内会判断是否需要执行System.gc()即full gc:
// 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) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// optimist!
if (tryReserveMemory(size, cap)) {
return;
}
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
// trigger VM's Reference processing
System.gc();
..........
代码:
关于Cleaner我们主要从这几点来理解:
1.继承自PhantomReference,关于PhantomReference类型我们就得来聊一聊java中的引用类型了:
jvm有四种引用: strong soft weak phantom final主要的区别体现在gc上的处理:
SoftReference<Object> soft = new SoftReference<>(new Object());
System.gc();
System.out.println(soft.get());
结果为: java.lang.Object@73ae9565
soft类型由于内存还充足,不会被回收。
WeakReference<Object> weak = new WeakReference<>(new Object());
WeakReference<String> weakString = new WeakReference<>("abc");
System.gc();
System.out.println(weak.get());
System.out.println(weakString.get());
结果为:
null
abc
weak类型在发生gc时就进行回收。weakString没被回收是引用常量池持有对"abc"的引用。
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>());
System.gc();
System.out.println(phantom.get());
结果为:null
PhantomReference的get方法总是返回null,因此无法访问对应的引用对象;其意义在于说明一个对象已经进入finalization阶段,可以被gc回收。
2.Cleaner定义成PhantomReference类型的目的是什么(引用自:http://lovestblog.cn/blog/2015/05/12/direct-buffer/)?
JDK里ReferenceHandler的实现:
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference r;
synchronized (lock) {
if (pending != null) {
r = pending;
Reference rn = r.next;
pending = (rn == r) ? null : rn;
r.next = r;
} else {
try {
lock.wait();
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
可见如果pending为空的时候,会通过lock.wait()一直等在那里,其中唤醒的动作是在jvm里做的,当gc完成之后会调用jvm方法VMGCOperation::doit_epilogue(),在方法末尾会调用lock的notify操作,至于pending队列什么时候将引用放进去的,其实是在gc的引用处理逻辑中放进去的。
3. Cleaner的ReferenceQueue属性和clean方法 从代码上可以看出,在调用create方法创建Cleaner时会将Cleaner的引用放入ReferenceQueue,在调用Cleaner的clean方法时会将自身引用从ReferenceQueue中移除,同时回收堆外内存。
这里可以思考一个问题,如果一直不调用Cleaner的clean方法,系统内存不是会爆?
其实通过上面的分析之后,这个问题我们不难回答,上面有讲过在创建DirectByteBuffer时会调用Bits.reserveMemory方法,该方法内部会判断内存情况,不足时会调用System.gc进行full gc操作。full gc时会扫描所有引用,包括DirectByteBuffer引用,查到DirectByteBuffer引用只有Cleaner引用指向它时(Cleaner又是PhantomReference类型的)会按照上面讲到的方式进行回收Cleaner和堆外内存。当然,这些需要满足一个前提条件是jvm没有添加-XX:+DisableExplicitGC参数来禁用System.gc。
关于堆外的介绍,先看一段R大在知乎上的解释(https://www.zhihu.com/question/57374068/answer/152691891):
Java NIO中的direct buffer(主要是DirectByteBuffer)其实是分两部分的:
Java | native
|
DirectByteBuffer | malloc'd
[ address ] -+-> [ data ]
|
其中 DirectByteBuffer 自身是一个Java对象,在Java堆中;而这个对象中有个long类型字段address,记录着一块调用 malloc() 申请到的native memory。所以回到题主的问题:
题主看的是OpenJDK的 sun.nio.ch.IOUtil.write(FileDescriptor fd, ByteBuffer src, long position, NativeDispatcher nd) 的实现对不对:
static int write(FileDescriptor fd, ByteBuffer src, long position,NativeDispatcher nd)throws IOException
{
if (src instanceof DirectBuffer)
return writeFromNativeBuffer(fd, src, position, nd);
// Substitute a native buffer
int pos = src.position();
int lim = src.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
try {
bb.put(src);
bb.flip();
// Do not update src until we see how many bytes were written
src.position(pos);
int n = writeFromNativeBuffer(fd, bb, position, nd);
if (n > 0) {
// now update src
src.position(pos + n);
}
return n;
} finally {
Util.offerFirstTemporaryDirectBuffer(bb);
}
}
这里其实是在迁就OpenJDK里的HotSpot VM的一点实现细节。HotSpot VM里的GC除了CMS之外都是要移动对象的,是所谓“compacting GC”。如果要把一个Java里的 byte[] 对象的引用传给native代码,让native代码直接访问数组的内容的话,就必须要保证native代码在访问的时候这个 byte[] 对象不能被移动,也就是要被“pin”(钉)住。可惜HotSpot VM出于一些取舍而决定不实现单个对象层面的object pinning,要pin的话就得暂时禁用GC——也就等于把整个Java堆都给pin住。HotSpot VM对JNI的Critical系API就是这样实现的。这用起来就不那么顺手。所以 Oracle/Sun JDK / OpenJDK 的这个地方就用了点绕弯的做法。它假设把 HeapByteBuffer 背后的 byte[] 里的内容拷贝一次是一个时间开销可以接受的操作,同时假设真正的I/O可能是一个很慢的操作。于是它就先把 HeapByteBuffer 背后的 byte[] 的内容拷贝到一个 DirectByteBuffer 背后的native memory去,这个拷贝会涉及 sun.misc.Unsafe.copyMemory() 的调用,背后是类似 memcpy() 的实现。这个操作本质上是会在整个拷贝过程中暂时不允许发生GC的,虽然实现方式跟JNI的Critical系API不太一样。(具体来说是 Unsafe.copyMemory() 是HotSpot VM的一个intrinsic方法,中间没有safepoint所以GC无法发生)。然后数据被拷贝到native memory之后就好办了,就去做真正的I/O,把 DirectByteBuffer 背后的native memory地址传给真正做I/O的函数。这边就不需要再去访问Java对象去读写要做I/O的数据了。就这样。