DirectByteBuffer
不同于Java Heap,这里的直接内存指堆外内存,JVM之外的一块区域,不受JVM的管控。
DirectByteBuffer该类本身还是位于Java内存模型的堆中。堆内内存是JVM可以直接管控、操纵。而DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在DirectByteBuffer一定会存在某种方式来操纵堆外内存。
在DirectByteBuffer的父类Buffer中有个address属性(堆外内存的地址),只会被直接缓存给使用到。之所以将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率。
——————————————————————————————————————
为什么:
1.对垃圾回收停顿的改善。
因为full gc 意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
2.在某些场景下可以提升程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。
创建DirectByteBuffer不如HeapByteBuffer快,但是本地IO(从操作系统本地获取数据,比如文件、socket网络数据)修改数据DirectByteBuffer是更快的。HeapByteBuffer的缓冲区是Java中的Java数组,使用HeapByteBuffer读取一个文件,操作系统会先把文件读取到操作系统管理的内存中。然后在把操作系统管理的这块内存的数据复制到jvm管理的内存(即HeapByteBuffer管理的Java字节数组)中,然后HeapByteBuffer对Java字节数组进行操作。而DirectByteBuffer(1)先在操作系统内核所管理的内存缓冲区中(文件系统页)开辟一块内存空间(作为缓冲区),文件数据可以直接从操作系统复制到该缓冲区中;(2)用户写入和读取数据就可以直接操作这块缓冲区,比使用HeapByteBuffer少了一个步骤,效率自然提升了。
——————————————————————————————————————
怎么用:
这里给出代码示例,使用堆内内存以及堆外内存创建bytebuffer:
public class TestByteBuffer {
public static void main(String[] args) {
directAndHeapSpeedCompare();
}
private static void directAndHeapSpeedCompare() {
int length = 10000;
directExecuteTime(length);
heapExecuteTime(length);
}
private static void directExecuteTime(int length) {
long startTime = System.currentTimeMillis();
ByteBuffer[] byteBufferArray = new ByteBuffer[length];
for (int i = 0; i < length; i++) {
byteBufferArray[i] = ByteBuffer.allocateDirect(1024);
}
long endTime = System.currentTimeMillis();
System.out.println("创建" + length + "个DirectByteBuffer所消耗的时间:" + (endTime - startTime));
}
private static void heapExecuteTime(int length) {
long startTime = System.currentTimeMillis();
ByteBuffer[] byteBufferArray = new ByteBuffer[length];
for (int i = 0; i < length; i++) {
byteBufferArray[i] = ByteBuffer.allocate(1024);
}
long endTime = System.currentTimeMillis();
System.out.println("创建" + length + "个HeapByteBuffer所消耗的时间:" + (endTime - startTime));
}
}
可以看到结果中,直接创建HeapByteBuffer是比DirectByteBuffer快的(一定范围内,如果HeapByteBuffer对象过多,引起GC,时间上不一定)。
另外,注意这里当内存大小超过了限制(将代码中的length调大),都会报出OOME,但两者的说明不同:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space...
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory...
——————————————————————————————————————
最佳实践:
堆外内存适用于生命周期中等或较长的对象。( 如果是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用造成的性能影响 )。
直接的文件拷贝操作,或者I/O操作。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操作,因为I/O操作是系统内核内存和设备间的通信,而不是通过程序直接和外设通信的。
同时,还可以使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到I/O操作的对象进行堆外内存的再使用。( Netty中就使用了该方式)。
——————————————————————————————————————
堆外内存与堆内内存池对比:
内存池:主要用于两类对象:①生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;②加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。
堆外内存:它和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。
——————————————————————————————————————
堆外内存的特点:
对于大内存有良好的伸缩性
对垃圾回收停顿的改善可以明显感觉到
在进程间可以共享,减少虚拟机间的复制
参考:
https://cloud.tencent.com/developer/article/1152616
https://www.cnblogs.com/felixzh/p/12013017.html