我们之前讲个ByteBuffer本身只是一个抽象类,真正创建对象还是通过他的方法来创建的。
其实一共只有三种方式
allocate:创建一个堆字节缓冲区来保存数据;
wrap:和前面allocate的区别在于,它是通过字节数组来创建的。也就是说你现在可以直接修改底层字节数组来改变值
allocateDirect:
查看allocateDirect源码:
这里讲解一个题外话,就是NIO底层只有部分源码是开源的可以看到,就比如这个DirectByteBuffer类一样,而sun公司的那部分源码是没有开源的,我们看不到,如果真的想要去看的话,去官网下载openjdk,虽然和oracle(sun公司被oracle收购)的jdk不是同一个东西,但是它们俩的实现是一样的方式,可以作为参考。如何查看openjdk源码请看我另外一篇文章:java并发编程--如何查看JVM中c/c++源码(vscode的使用)
继续往下走,我们会发现unsafe这个方法是native本地方法,通过JNI(java native interface)的方式调用
在这里你就得记住了哦,直接缓存和间接缓存一个区别就是,直接缓存调用的native方法来实现的。
向之前的heapbuffer都是使用的java的对象来实现缓存中,还有那个wrap实现使用的是字节数组。
现在讲解一下DirectBuffer底层实现:
首先我们知道directByteBuffer是一个通过java在堆中new出来的,这个毫无疑问,而native在本地系统内存中,这种情况下你就得考虑一个问题了?一个是jvm虚拟机内容中的数据,一个是操作系统内存中的数据,这种情况下你怎么实现他们的交互呢?
首先java内存通过调用native方法,将数据存储到malloc(该函数是c中的函数)中。
但是这个存储之间的联系是怎样进行的呢?
然后我们直接找到directBuffer的父类,发现了address变量:里面对它的描述就是,它只会被直接缓存(directBuffer)所使用到。而且描述说到,放到这里的目的为的是使得通过JNI调用的时候速度更快。
而且现在应该也很好能够理解到这个地址其实就是为了引用堆外内存中的数据。
总结一下,就是directBuffer本身是在jvm虚拟机堆中创建的,但是它所需要的存储数据的空间是在堆外内存分配,也就是操作系统中分配,这时候要调用该内存就需要使用directBuffer的父类中的变量address来进行调用。
这个时候有人就问了,为什么要这么麻烦将数据放入堆外内存,而不是放到堆内存中呢?
主要是考虑到效率问题,在堆外内存也就是操作系统内存中的执行效率远远比在堆内存这种执行的效率更快。
为什么效率更高呢?
主要是因为在程序中进行IO处理或者NIO处理的是,如果采用heapBuffer进行存储数据的话,操作系统不会直接在堆内存中操作那个字节数组,而是将字节数组拷贝到堆外内存中,然后在进行处理。这样的话,很明显将字节数组放入到堆内存中比放在堆外内存中处理多了一次操作。
这种直接缓存方式就是很多人所说的零拷贝。
但是有些人就会说了,间接缓存效率不高的原因不就是因为将字节数组拷贝到堆外内存中,那我们如果直接让操作系统直接操作堆内存不就可以了吗?
这个正常的操作确实可以,操作系统肯定是可以直接操作java的虚拟机的,但是问题出在了虚拟机的垃圾回收机制,在虚拟机的垃圾回收机制中,有一种常用的回收算法是标记-整理算法:具体详情可以看我另外一篇文章:JVM虚拟机深入理解----垃圾回收机制的深入浅出
主要问题出现了整理上面,为了方便垃圾回收,将会将标记和未标记的数据进行重排序,这样导致操作系统通过地址调用堆内存的时候,因为垃圾回收机制导致地址在不断的发生改变。导致这种直接通过操作系统操作堆内存的方式行不通。
因此只能使用另外一种方式,将java中的字节数组拷贝到堆外内存中,操作系统就可以直接进行操作了。而且在这个拷贝的过程中,jvm会做一个保障,让他不会受到GC的影响。
这个时候又会有人提出问题,既然jvm可以保障,那可以直接将字节数组在堆内存中进行保障不就行了吗?这其实就是一个效率对比的问题了。在jvm虚拟机中,将字节数组拷贝到操作系统中,它的速度是很快的,而且在这个拷贝的过程中,操作系统连接IO系统的等待时间远远高于这个拷贝时间,所以综合考虑拷贝这点时间和性能消耗根本不值得一提。