持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情
一、堆外内存的理解和堆外内存的优势
堆内和堆外的概念
如何用堆外内存?
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
- 传入的是你要申请的堆外内存的大小
- 你可以直接把你的数据写入到堆外内存,DirectByteBuffer 里去
- 把这块数据通过Socket发送,就是直接发送就可以了,不需要走一个拷贝过程
读写文件也是同理的,都可以节约数据拷贝次数。
二、JDK是如何对堆外内存进行分配和回收的?会发生堆外内存溢出么?
-XX:MaxDirectMemorySize:通过 JVM 参数是可以设置你最大可以使用的堆外内存的大小的,比如说设置堆外内存最大可以使用 1GB,此时已经使用了 950MB 空间了,然后呢,你此时要申请一块 80MB 的堆外内存,会发现说,堆外内存已经不够了,此时不能直接分配堆外内存了。
DirectByteBuffer,这个对象是 JVM 堆内存里的一个对象,但是这个 DirectByteBuffer 里面包含指针,引用了一块堆外的内存。
- 如果堆外内存足够,就直接预留一部分内存
- 如果堆外内存不足,则将已经被 JVM 垃圾回收的 DirectBuffer 对象的堆外内存释放
- 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()
- 如果9次尝试后依旧没有足够的可用堆外内存,则抛异常
- 实际分配内存
DirectByteBuffer 回收,就会回收关联的堆外内存,或者走内部有一个 cleanr 对象,可以用反射获取它,然后调用它的 clean 方法来主动释放内存。
如果依靠 jvm gc 机制,可能 DirectByteBuffer 躲过 N 次 minor gc 进入了老年代,然后老年代迟迟没有放满,因此迟迟没有回收,此时可能会导致 DirectByteBuffer 对象一直在引用堆外内存。
这样当你要分配更多的堆外内存时,无法腾出来更多的内存,就会有堆外内存溢出了。
三、如果不使用零拷贝技术,普通的IO操作在OS层面是如何执行的?
File file = new File("xxx.txt");
RandomAccessFile raf = new RandomAccessFile(file,"rw");
byte[] arr = new byte[(int)file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
使用 read 读取数据的时候,会有一次用户态到内核态的切换,也就是说从用户角度切换到内核角度去执行,这个时候基于 DMA 引擎把磁盘上的数据拷贝到内核缓冲里去;接着会从内核态切换到用户态,基于 CPU 把内核缓冲里的数据拷贝到用户缓冲区里去。
接着我们调用了 Socket 的输出流的 write 方法,此时会从用户态切换到内核态,同时基于 CPU 把用户缓冲区里的数据拷贝到 Socket 缓冲区里去,接着会有一个异步化的过程,基于 DMA 引擎从 Socket 缓冲区里把数据拷贝到网络协议引擎里发送出去。
都完了之后,从内核态切换到用户态
所以说,从本地磁盘读取数据,到通过网络发送出去,用户态和内核态之间,要发生4次切换,这是其一;其二,数据从磁盘拿出来过后,一共要经过4次拷贝;所以说这4次切换和4次拷贝,让普通的IO操作都性能较低。
四、听说过mmap吗?内存映射技术为什么可以提升IO性能?
把一个磁盘文件映射到内存里来,然后把映射
有一种 mmap 技术,也就是内存映射,直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于 DMA 引擎拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了。
光是这一点,就可以避免一次拷贝了,但是这个过程中还是会用户态切换到内核态去进行映射拷贝,接着再次从内核态切换到用户态,建立用户缓冲区和内核缓冲区的映射。
接着把数据通过 socket 发送出去,还是要再次切换到内核态。
接着直接把内核缓冲区里的数据拷贝到 socket 缓冲区里去,然后再拷贝到网络协议引擎里,发送出去就可以了,最后切换回用户态。
减少一次拷贝,但是并不减少切换次数,一共是4次切换,3次拷贝。
mmap 技术主要在 RocketMQ 里来使用的。
五、零拷贝技术到底是什么?它是如何提升IO性能的?
linux 提供了 sendfile,也就是零拷贝技术
在你的代码里面,如果说你基于零拷贝技术来读取磁盘文件,同时把读取到的数据通过 socket 发送出去的话,流程如下,kafka源码,transferFrom 和 transferTo 两个方法,从磁盘上读取文件,把数据通过网络发送出去。
这个零拷贝技术,就是先从用户态切换到内核态,在内核态的状态下,把磁盘上的数据拷贝到内核缓冲区,同时从内核缓冲区拷贝一些 offset 和 length 到 socket 缓冲区;接着从内核态切换到用户态,从内核缓冲区直接把数据拷贝到网络协议引擎里去。
同时从 Socket 缓冲区里拷贝一些 offset 和 length 到网络协议引擎里去,但是这个 offset 和 length 的量很少,几乎可以忽略。
只要2次切换,2次拷贝,就可以了。
kafka、tomcat、都是用的零拷贝技术,rocketmq 用的是 mmap技术,mmap 还是要多2次切换和1次拷贝的,在java代码中如何进行mmap和零拷贝,可以去网上看一些资料。
用户态(用户进程)和内核态(os 进程)、用户空间(用户缓冲区)和内核空间(内核缓冲区)