Netty中关于Direct Buffers的问题思考

2019-05-01  本文已影响0人  叫我不矜持

JDK1.4中新加入了NIO,引入了一种基于通道(Channel)和缓存区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存(native堆),然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

直接内存(Direct Memory),直接内存并不是运行时数据区的一部分,也不是Java虚拟机规范中定义的内在区域。它通过Unsafe类的allocateMemory()方法申请分配内存,底层会调用操作系统的的malloc函数。

关于Direct Buffers中的问题涉及到许多...
下面我将这些问题的答案罗列如下,回答全部来自互联网,外加自己的一些理解,如果有不到位的地方,敬请批评指正!

1.1 NIO 是如何分配Native Memory的

NIO 使用java.nio.ByteBuffer.allocateDirect()方法分配内存, 这种方式也就是通常所说的NIO direct memory 。
ByteBuffer.allocateDirect()分配的内存使用的是本机内存而不是Java 堆上的内存,这也进一步说明每次分配内存时会调用操作系统的malloc()函数。

malloc函数分配内存主要是使用brk和mmap系统调用,brk是_edata指针堆中的地址往高地址推,mmap是在堆和栈之间找分配一块空闲的虚拟内存。brk和mmap分配的都不是物理内存,当第一次访问分配的虚拟内存的时候,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。这时操作系统再负责分配物理内存,从磁盘空间加载数据到该物理内存中,建立虚拟内存和直接内存的映射关系。

1.2 bck和mmap的使用场景
当使用malloc分配的字节数小于128k的时候,调用brk分配虚拟内存
当malloc分配的字节数大于128的时候,使用过mmap分配虚拟内存

1.3 为什么等到第一次访问虚拟内存的时候才分配物理内存呢?
因为申请的内存不一定马上使用,推迟分配可以系统拥有更多的空闲物理内存去出来其他事,从而提高系统的吞吐量。

1.4 内核空间与用户空间的概念?
为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。内核空间主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源等的程序逻辑。

1.5 用户空间和内核空间的区分?

地址映射关系.png

用户空间和内核空间的区分一般采用虚拟内存来实现,因此用户空间和内核空间都是在虚拟内存中。

使用虚拟内存无非是因为其两大优势:
一是它可以使多个虚拟内存地址指向同一个物理内存;
二是虚拟内存的空间可以大于物理内存的空间。

1.6 为何需要内存空间和用户空间的划分呢?
很显然和前面所说的每个进程都独立使用属于自己的内存一样,为了保证操作系统的稳定性,运行在操作系统中的用户程序不能访问操作系统所使用的内存空间。这也是从安全性上考虑的,如访问硬件资源只能由操作系统来发起,用户程序不允许直接访问硬件资源。如果用户程序需要访问硬件资源,如网络连接等,可以调用操作系统提供的接口来实现,这个调用接口的过程也
就是系统调用。每一次系统调用都会存在两个内存空间的切换,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户空间的数据复制很费时,虽然保住了程序运行的安全性和稳定性,但是也牺牲了一部分效率。但是现在已经出现了很多其他技术能够减少这种从内核空间到用户空间的数据复制的方式,如Linux系统提供了sendfile 文件传输方式。
注意,只有内核空间的内存才能被DMA引擎独立异步地存取。

1.7 DirectBuffer的开辟的堆外内存解决了什么问题?
HeapByteBuffer是在jvm的内存范围之内,然后在调io的操作时会将数据区域拷贝一份到os的内存区域,这样造成了不必要的性能上的降低,这样做是有原因的,试想假设如果os和jvm都是用jvm里边的数据区域, 但是jvm会对这块内存区域进行GC回收,可能会对这块内存的数据进行更改,根据我们的假设,由于这块区域os也在使用,jvm对这块共享数据发生了变更,os那边就会出现数据错乱的情况。那么如果不让jvm对这块共享区域进行GC是不是可以避免这个问题呢?
答案是不行的,也会存在问题,如果jvm不对其进行GC回收,jvm这边可能会出现OOM的内存溢出。因此,最后这个地方非常尴尬,只能拷贝jvm的那一份到os的内存空间,即使jvm那边的数据区域被改变,但是os里边的不会受到影响,等os使用io结束后会对这块区域进行回收,因为这是os的管理范围之内。

1.8 DirectByteBuffer 的原理?
其中 DirectByteBuffer 自身是一个Java对象,在Java堆中;而这个对象中有个long类型字段address,记录着一块调用 malloc() 申请到的native memory。
IO的时候只需要把 DirectByteBuffer 背后的native memory地址传给真正做I/O的函数。这边就不需要再去访问Java对象去读写要做I/O的数据了。
这块内存直接与io设备进行交互,当jvm对DirectByteBuffer内存垃圾回收的时候,会通过address调os,os将address对应的区域回收。

1.9.DirectBuffer 属于堆外存,那应该还是属于用户内存,还是内核内存?
DirectByteBuffer 自身是(Java)堆内的,它背后真正承载数据的buffer是在(Java)堆外——native memory中的。这是 malloc() 分配出来的内存,是用户态的。

1.10 如果是DirectBuffer 指向的内存在用户空间,是不是还存在I/O的数据从内核空间到用户空间(native堆)的数据拷贝?
首先,访问硬件资源只能由操作系统来发起,用户程序不允许直接访问硬件资源。所以真实的IO操作,需要调用操作系统提供的接口来实现,这个调用接口的过程也就是系统调用。

采用JVM内存的方式:
如果采用read/write的方式,那么必然产生内核缓冲区到用户缓冲区的过程。拿网络传输来说,read的过程如下:
协议引擎—>内核socket缓冲区—>内核缓冲区—>用户地址空间的堆外内存—>Java堆内存—>处理
这个过程的堆外内存存在的意义见问题1.7

采用堆外内存的方式:
用堆外内存只需拷贝一次,而用堆内存是要拷贝两次(内核->堆外->堆)
协议引擎—>内核socket缓冲区—>内核缓冲区—>用户地址空间的堆外内存—>处理

操作系统结构图.png
上一篇下一篇

猜你喜欢

热点阅读