Netty基础系列 --堆外内存与零拷贝详解

Posted java微技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty基础系列 --堆外内存与零拷贝详解相关的知识,希望对你有一定的参考价值。

到目前为止,我们知道Nio当中有三个最最核心的组件,分别是:Selelctor,Channel,Buffer。 

我们现在来深入理解一下Buffer在 堆内创建内存 和 堆外创建内存 的底层原理,与 零拷贝 的具体实现。



2|0Buffer



Buffer是一个抽象类,首先我们来看看Buffer有哪些实现类。

我们从上面这张截图可以看出,Buffer的直接子类有7种。除了Java中Boolean类型。剩余的7种基本类型都有与之对应的Buffer。不同类型的Buffer存储的内容也不同,比如说ByteBuffer存储的就是byte。IntBuffer存储的就是int。不要想得太复杂,把底层想象成数组即可。


接下来我们着重对ByteBuffer来进行讲解。理解了一个其他的理解起来都差不多。

首先我们来看ByteBuffer的继承关系图

Netty基础系列(4) --堆外内存与零拷贝详解

由上面的继承关系图可以看出,ByteBuffer的子类有五个,分别为:

HeapByteBuffer:代表的是jvm堆内的缓存。
HeapByteBufferR: 代表的是jvm堆内的只读缓存。
MappedByteBuffer:直接缓存的抽象基类。
DirectByteBuffer:代表的是操作系统内存的缓存。
DirectByteBufferR:代表的是操作系统内存的只读缓存

上面这几个类看名字和我的介绍我想你应该知道有什么区别了,这里其实只分为两大类。
分配在堆内存的缓存和分配在操作系统内存的缓存。

2|1HeapByteBuffer



我们首先来看在堆内分配缓存的底层原理。

先来看一段代码。

    public static void main(String args[]){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
}

我们直接调用ByteBuffer的静态方法创建了一个1024个字节的ByteBuffer缓存。那么ByteBuffer的静态方法allocate()在底层到底做了些什么呢?

我们再来看看ByteBuffer类对于静态方法allocate()的实现。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
}

没错,就是很简单。直接new了一个HeapByteBuffer对象,并指定大小为1024个字节。这里暂时不用管capacity是什么,后面我们会详细的讲解,在这里capacity就是我们传入的1024。

到目前为止,我们已经创建了一个HeapByteBuffer对象。我们创建这个对象的意义就是用来对Channel进行读写。此时我们内存模型已经变成了如下图所示:

Netty基础系列(4) --堆外内存与零拷贝详解

对照着上图我们再来看看之前写的这个方法。

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

首先再栈空间的某个栈帧中创建了byteBuffer,接着将其指向堆内存中的对象HeapByteBuffer。

好了接下来是我们的重点!!!!

此时操作系统会自动在JVM之外的内存中分配一块内存空间,这部分内存空间的创建和销毁完全由操作系统来管理。我们无需在意。

Channel的数据无论是读还是写都是与操作系统分配的这块内存打交道而不是我们的堆内存,当准备读数据的时候,Channel将数据读到操作系统分配的内存中,然后再复制到JVM堆内存中的HeapByteBuffer对象中。写操作也是如此,当我们修改了HeapByteBuffer的数据,会将修改后的数据复制到操作系统分配的内存中,然后再写到Channel中。

我们之前学的普通的IO操作底层基本上都是如此,我们思考一下,为什么不能直接将Channel怼到HeapByteBuffer中呢?

2|2DirectByteBuffer



我们来看看在堆外分配内存是如何实现的。

与前文一样,我们首先来看在操作系统中直接分配内存的底层原理。先来看一段代码。

    public static void main(String args[]){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
}

与创建堆内缓存类似,我们直接调用ByteBuffer的静态方法创建了一个1024个字节的DirectByteBuffer缓存。那么ByteBuffer的静态方法allocateDirect()方法与allocate()方法又有什么区别呢?

我们再来看看ByteBuffer类对于静态方法allocateDirect()的实现。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
}

这里也是直接new了一个DirectByteBuffer对象,我们进入该对象的构造函数看看干了些什么

这就是我们大名鼎鼎的zero copy(零拷贝)技术。

3|0总结



其实我们多思考一下,这样的优势大吗?其实Channel中IO的操作相对于内存的复制来说是慢很多的,即便我们在读写数据的时候多了两次复制的过程对于整体来说影响是不大的。

那么什么时候就会体现出零拷贝的优势呢?有大量并发io操作,并且io操作是短暂完成的。这时由于节省了大量的内存copy操作,这些节省的时间积累下来也是非常可观的。

netty的底层就是用的零拷贝技术,所以netty能做到很好并发,之后我们会分析在netty中零拷贝是如何落实的。


以上是关于Netty基础系列 --堆外内存与零拷贝详解的主要内容,如果未能解决你的问题,请参考以下文章

Netty基础系列 --彻底理解NIO

「Netty系列」彻底弄清网络七层协议TCP/IPIPTCP(Netty前置一)

#yyds干货盘点#netty系列之:好马配好鞍,为channel选择配套的selector

Netty系列相关面试题汇总

netty系列之:使用netty搭建websocket服务器

#yyds干货盘点#netty系列之:请netty再爱UDT一次