java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别

使用之前写的文章里的例子

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 中。

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第1张图片

间接调用 Util 的 getTemporaryDirectBuffer() 获取临时的 DirectByteBuffer,使用完毕后销毁。

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第2张图片

可见,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);
    }
}

直接在堆外分配的内存缓冲区,在构造器中有三个操作,如下图

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第3张图片

向 Bits 中的变量设置默认值

获取 DirectByteBuffer 的最大直接内存

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第4张图片

在 VM 类中可以看到,默认最大值是 64MB,可以通过参数 -XX:MaxDirectMemorySize 进行修改,具体修改参数修改可以参见如下的官方文档

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第5张图片

接下来是给 Bits 中的变量赋值

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第6张图片

因为参数 size 和 cap 是创建 ByteBuffer 时指定的,totalCapacity 默认值为 0,加上 maxMemory 的值为 64 MB,所以比较式右边的值为 64 MB,cap <= 右边的数据,所以进入循环处理,第一次就停止循环到被调用方停止当前执行方法。

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第7张图片

通过 Unsafe 进行分配内存

调用 Unsafe 的 allocateMemory() 先向内存中分配一个新块

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第8张图片

调用 Unsafe 的 setMemory() 向分配的内存块中设置对应的字节

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第9张图片

创建内存清理者

其中创建的 Deallocator 是一个内部类,创建的对象作为一个后台线程处理。Cleaner 是一个 PhantomReference 对象,即对应的值无法获取。

线程执行的时候会触发 Unsafe 的 freeMemory() 和 Bits.unreserveMemory() 触发堆外的内存清理操作,但是触发时间无法控制。

java中的缓冲类HeapByteBuffer和DirectByteBuffer的区别_第10张图片

总结

通过分析可以得出如下

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

你可能感兴趣的:(java,jvm,垃圾收集,java,jvm)