设为首页 加入收藏

TOP

【Netty】浅谈ByteBuf对JDK原生ByteBuffer之优化(一)
2019-05-13 14:32:14 】 浏览:168
Tags:Netty 浅谈 ByteBuf JDK 原生 ByteBuffer 优化
版权声明:copyright by Shannon https://blog.csdn.net/Shannon076/article/details/89226419

写在前面

在Java NIO的体系中,Buffer是其中非常重要的一个组件,是通道Channel和消息数据之间传输的纽带,即:所有写操作必须将数据写到Buffer中,所有读操作必须从Buffer中读。这种做法相比较于传统的BIO基于字节/字符流的操作方式,利用缓冲区的作用,大大提高了IO的效率,但是这只是NIO优于BIO的其中一个方面,另外其他诸如多路复用器Selector,如果有兴趣,大家可以自己学习相关知识,这篇文章将主要从Buffer的角度来阐述一些NIO中的基本概念。

原生Buffer中的概念

在NIO的Buffer体系中最常用的就是ByteBuffer,下面的所有示例都是基于ByteBuffer来进行的。在写这篇博客之前,作者默认大家已经掌握了Buffer的基本用法,如allocate、put、get、flip、clear等方法,以及position、capacity、limit、mark等标记字段的含义等。那么,接下来我们就来深入探究JDK原生的ByteBuffer是如何提升IO性能的吧。我们先来看一下Buffer中的初始化方式,在ByteBuffer中共有两种初始化的方式,分别初始化两种不同类型的Buffer对象,分别为HeapByteBuffer和DirectByteBuffer。

public static ByteBuffer allocate(int capacity) {
	if (capacity < 0)
    	throw new IllegalArgumentException();
   return new HeapByteBuffer(capacity, capacity);
}
public static ByteBuffer allocateDirect(int capacity) {
     return new DirectByteBuffer(capacity);
}

那么这两种不同的Buffer分别是什么呢?请继续看…

堆内存HeapByteBuffer

堆内存,大家非常熟悉,JVM内存中,一般对象(包括数组)都是分配在堆中,JVM的GC可以回收分配在堆中的垃圾对象。

直接内存DirectByteBuffer

在Java中可以通过unsafe类绕过JVM内存直接在系统的物理内存中分配一块内存区域,然后在堆中初始化一个DirectByteBuffer对象引用这块内存,因此对堆中生成的DirectByteBuffer对象的操作就可以映射成对堆外内存的操作。这里还需要进行补充的是:直接内存的垃圾回收只有在触发Full GC的时候才会进行,而这取决于堆内存中DirectByteBuffer的大小。了解这点非常重要,这对理解Netty中的内存池有非常大的帮助。

到这里,我们就介绍了跟Buffer相关的两个重要的基本概念。另外跟DirectByteBuffer相关的还有一个应用广泛的概念——零拷贝(zero-copy)

零拷贝

直接内存的分配取决于操作系统,受操作系统管理,因此对直接内存的管理是在操作系统内核态进行。通常情况下通过网络进行数据发送的一般流程如图所示,过程如下:

  • 从数据源(文件、磁盘等)将数据copy到内核缓冲区(其实是一个临时的直接内存);
  • 内核将准备好的数据copy到用户空间(OS的第一次状态切换);
  • 将数据copy到socket缓冲区(OS的第二次状态切换
  • 将socket缓冲区中的数据copy到网卡缓冲区。
    在这里插入图片描述

根据上述的过程描述,在进行数据发送的时候,一共发生了4次数据copy、2次状态切换,这都是非常损耗性能的操作。

而直接内存的使用又是怎么提升性能的呢?

  • 直接内存存在于内核态,因此不需要copy到用户空间就可以直接传输给socket缓冲区,相当于上述过程中的第2个步骤没有了,减少了1次copy和1次状态切换;
  • 相应的步骤3的状态切换也没有,整个数据发送的过程变成下图的形式。
    在这里插入图片描述

整个数据发送的过程中,数据copy和状态切换的次数降低到3次和0次。因此,零拷贝的应用,大大提高了IO的性能。

抛出一个问题

在零拷贝部分,我们在介绍传统数据传输过程的时候,在步骤1中介绍说将数据copy到一个内核缓冲区,而这个内核缓冲区实际上就是一个临时的DirectByteBuffer,现在的问题是,为什么需要这样的一个临时的DirectByteBuffer?在解释这个问题之前,先来看看原生Buffer中关于这一点在代码中是如何体现的。下面代码展示的是IOUtil类中的write方法。

static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1 instanceof DirectBuffer) {
            return writeFromNativeBuffer(var0, var1, var2, var4);
        } else {
            int var5 = var1.position();
            int var6 = var1.limit();
            assert var5 <= var6;
            int var7 = var5 <= var6  var6 - var5 : 0;
           ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
            int var10;
            try {
                var8.put(var1);
                var8.flip();
                var1.position(var5);
                int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
                if (var9 > 0) {
                    var1.position(var5 + var9);
                }
                var10 = var9;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(var8);
            }
            return var10;
        }
    }

上述代码的大概流程是这样的:

  • 往Buffer里面写数据;
  • 如果Buffer是DirectBuffer(基于直接内存)的,那么直接写就可以了;
  • 如果Buffer不是基于直接内存的,那一定就是基于堆内存的,那么这时候需要构造一个临时的DirectBuffer,然后先将数据写到临时的DirectBuffer中。

是不是很奇特?至于为什么会这样,可以自己思考一下,在下一篇文章中我会给大家详细介绍。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Git冲突:commit your changes or.. 下一篇Kafka--Java--Demo

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目