使用之前写的文章里的例子
https://blog.csdn.net/zlpzlpzyd/article/details/135292683
HeapByteBuffer
import java.io.File;
import java.io.FileInputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class TestHeapByteBuffer implements Serializable {public static void main(String[] args) {long start = System.currentTimeMillis();File file = new File(Control.PATH);ByteBuffer byteBuffer = ByteBuffer.allocate(Control.SIZE);try (FileChannel fileChannel = new FileInputStream(file).getChannel()) {while (fileChannel.read(byteBuffer) > 0) {byteBuffer.clear();}} catch (Throwable e) {e.printStackTrace();}long duration = System.currentTimeMillis() - start;System.out.println(duration);}
}
HeapByteBuffer 在堆上创建的缓冲区,通过 FileChannel 的 read() 读取缓冲区时,会先通过 IOUtil.read() 将 ByteBuffer 获取一个临时 DirectByteBuffer 添加到原来的 ByteBuffer 中。
间接调用 Util 的 getTemporaryDirectBuffer() 获取临时的 DirectByteBuffer,使用完毕后销毁。
可见,HeapByteBuffer 使用的缓冲区不是单纯在堆上处理,还需要借助于 DirectByteBuffer 来处理。
这样就面临一个问题,每次调用 read() 都会造成一个开销问题。
上面只是拿了 read() 来讲解,write() 类似。
DirectByteBuffer
import java.io.File;
import java.io.FileInputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class TestDirectByteBuffer implements Serializable {public static void main(String[] args) {long start = System.currentTimeMillis();File file = new File(Control.PATH);ByteBuffer byteBuffer = ByteBuffer.allocateDirect(Control.SIZE);try (FileChannel fileChannel = new FileInputStream(file).getChannel()) {while (fileChannel.read(byteBuffer) > 0) {byteBuffer.clear();}} catch (Throwable e) {e.printStackTrace();}long duration = System.currentTimeMillis() - start;System.out.println(duration);}
}
直接在堆外分配的内存缓冲区,在构造器中有三个操作,如下图
向 Bits 中的变量设置默认值
获取 DirectByteBuffer 的最大直接内存
在 VM 类中可以看到,默认最大值是 64MB,可以通过参数 -XX:MaxDirectMemorySize 进行修改,具体修改参数修改可以参见如下的官方文档
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
接下来是给 Bits 中的变量赋值
因为参数 size 和 cap 是创建 ByteBuffer 时指定的,totalCapacity 默认值为 0,加上 maxMemory 的值为 64 MB,所以比较式右边的值为 64 MB,cap <= 右边的数据,所以进入循环处理,第一次就停止循环到被调用方停止当前执行方法。
通过 Unsafe 进行分配内存
调用 Unsafe 的 allocateMemory() 先向内存中分配一个新块
调用 Unsafe 的 setMemory() 向分配的内存块中设置对应的字节
创建内存清理者
其中创建的 Deallocator 是一个内部类,创建的对象作为一个后台线程处理。Cleaner 是一个 PhantomReference 对象,即对应的值无法获取。
线程执行的时候会触发 Unsafe 的 freeMemory() 和 Bits.unreserveMemory() 触发堆外的内存清理操作,但是触发时间无法控制。
总结
通过分析可以得出如下
HeapByteBuffer 使用简单,每次执行数据读取写入间接创建 DirectByteBuffer,效率低。
DirectByteBuffer 使用相对麻烦,但是效率高,需要考虑到通过 ByteBuffer 分配的缓冲区与 jvm 参数 -XX:MaxDirectMemorySize 是否合理的问题,不然的在运行过程中会出现内存溢出问题。内部是通过 PhantomReference(Cleaner 的父类)来处理创建的缓冲区,最终通过 Reference 的 clean() 来间接执行堆外内存回收。
为什么使用 DirectByteBuffer
摘自网络上的解答
https://cheng-dp.github.io/2018/12/11/direct-memory-and-direct-byte-buffer/
减少复制操作,加快传输速度
HotSpot虚拟机中,GC除了CMS算法之外,都需要移动对象。
在NIO实现中(如: FileChannel.read(ByteBuffer dst), FileChannel.write(ByteByffer src)), 底层要求连续的内存,且使用期间不得变动, 如果提供的Buffer是HeapByteBuffer,为了保证在数据传输时,被传输的byte数组背后的对象不会被GC回收或者移动,JVM会首先将堆中的byte数组拷贝至直接内存,再由直接内存进行传输。
那么,相比于HeapByteBuffer在堆上分配空间,直接只用DirectByteBuffer在直接内存分配就节省了一次拷贝,加快了数据传输的速度。
减少GC压力
虽然GC仍然管理DirectByteBuffer,但基于DirectByteBuffer分配的空间不属于GC管理,如果IO数量较大,可以明显降低GC压力。
http://lovestblog.cn/blog/2015/05/12/direct-buffer/
DirectByteBuffer在创建的时候会通过Unsafe的native方法来直接使用malloc分配一块内存,这块内存是heap之外的,那么自然也不会对gc造成什么影响(System.gc除外),因为gc耗时的操作主要是操作heap之内的对象,对这块内存的操作也是直接通过Unsafe的native方法来操作的,相当于DirectByteBuffer仅仅是一个壳,还有我们通信过程中如果数据是在Heap里的,最终也还是会copy一份到堆外,然后再进行发送,所以为什么不直接使用堆外内存呢。对于需要频繁操作的内存,并且仅仅是临时存在一会的,都建议使用堆外内存,并且做成缓冲池,不断循环利用这块内存。
DirectByteBuffer 的使用场景
需要频繁操作的小内存,并且仅仅是临时存在一会的,都建议使用堆外内存,并且做成缓冲池,不断循环利用这块内存。
DirectByteBuffer 使用注意事项
摘自网络上的解答
创建和销毁比普通Buffer慢。
虽然DirectByteBuffer的传输速度很快,但是创建和销毁比普通Buffer慢。因此DirectByteBuffer适合只是短时使用需要频繁创建和销毁的场合。
使用直接内存要设置-XX:MaxDirectMemorySize指定最大大小。
直接内存不受GC管理,而基于DirectByteBuffer对象的自动回收过程并不稳定,如DirectByteBuffer对象被MinorGC经过MinorGC进入老年代,但是由于堆内存充足,迟迟没有触发Full GC,DirectByteBuffer将不会被回收,其申请的直接内存也就不会被释放,最终造成直接内存的OutOfMemoryError。
如果我们大面积使用堆外内存并且没有限制,那迟早会导致内存溢出,毕竟程序是跑在一台资源受限的机器上,因为这块内存的回收不是你直接能控制的,当然你可以通过别的一些途径,比如反射,直接使用Unsafe接口等,但是这些务必给你带来了一些烦恼,Java与生俱来的优势被你完全抛弃了—开发不需要关注内存的回收,由gc算法自动去实现。另外上面的gc机制与堆外内存的关系也说了,如果一直触发不了cms gc或者full gc,那么后果可能很严重。
参考链接
https://www.zhihu.com/question/60892134
https://www.cnblogs.com/Chary/p/16718014.html
https://developer.aliyun.com/article/763697
https://juejin.cn/post/6844903744119783431
https://www.infoq.cn/news/2014/12/external-memory-heap-memory/
https://blog.csdn.net/flyzing/article/details/115388720