美文网首页
Netty 源码分析 —— NIO 基础(五)之零拷贝与其它源码

Netty 源码分析 —— NIO 基础(五)之零拷贝与其它源码

作者: 小安的大情调 | 来源:发表于2020-02-14 23:10 被阅读0次

    我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》

    [TOC]
    Thinking

    1. 一个技术,为什么要用它,解决了那些问题?
    2. 如果不用会怎么样,有没有其它的解决方法?
    3. 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
    4. 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
    5. 这些问题你又如何去解决的呢?

    本文基于Netty 4.1.45.Final-SNAPSHOT

    1、NIO堆外内存与零拷贝

    NIO堆外内存

    ​ 在上述NIO Buffer 讲解中,我们隐约的提到过为什么要使用Direct Buffer小节中提到过直接内存(堆外内存)与堆内存(Non - Direct Buffer)的区别:

    这里会涉及到 Java 的内存模型

    Direct Buffer:

    • 所分配的内存不在 JVM 堆上, 不受 GC 的管理.(但是 Direct Buffer 的 Java 对象是由 GC 管理的(会将内存地址映射到一个标记上), 因此当发生 GC, 对象被回收时, Direct Buffer 也会被释放)
    • 因为 Direct Buffer 不在 JVM 堆上分配, 因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存, 但是 JVM 不好统计到非 JVM 管理的内存.)
    • 申请和释放 Direct Buffer 的开销比较大. 因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer, 然后不断复用此 buffer, 在程序结束后才释放此 buffer.
    • 使用 Direct Buffer 时, 当进行一些底层的系统 IO 操作时, 效率会比较高, 因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中.

    Non-Direct Buffer:

    • 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
    • 因为 Non-Direct Buffer 在 JVM 堆中, 因此当进行操作系统底层 IO 操作中时, 会将此 buffer 的内存复制到中间临时缓冲区中. 因此 Non-Direct Buffer 的效率就较低.

    总结对比:

    • 之所以使用堆外内存,是为了避免每次使用buffe如对象时,都会将此对象复制到中间林是缓冲区中,因此Non-Direct Buffer效率会非常低下。
    • 堆外内存(直接内存--direct byte buffer)则可以直接使用,避免了对象的复制,提高了效率。

    基于上述总结,我们先看一下下面创建Buffer 的两种方法的代码:

        @Test
        public void test01() throws Exception {
            FileInputStream in = new FileInputStream("src/main/resources/data/DirectorBuffer.txt");
            FileOutputStream out = new FileOutputStream("src/main/resources/data/DirectorBuffer-out.txt");
    
            // 获取文件Channel
            FileChannel inChannel = in.getChannel();
            FileChannel outChannel = out.getChannel();
    
            // 普通获取Buffer
            ByteBuffer allocate = ByteBuffer.allocate(1024);
    
            // 获取 堆外内存 Buffer
            ByteBuffer allocateDirect = ByteBuffer.allocateDirect(1024);
    
            // 从源码 分析两种的区别。
            int count = inChannel.read(allocate);
            while (count != -1) {
                log.info("read :{}", count);
                allocate.flip();
    
                outChannel.write(allocate);
                allocate.clear();
                // 防止死循环
                count = inChannel.read(allocate);
            }
            inChannel.close();
            outChannel.close();
        }
    }
    
    • ByteBuffer.allocate(1024);跟随进入源码:

    •     public static ByteBuffer allocate(int capacity) {
              if (capacity < 0)
                  throw new IllegalArgumentException();
              return new HeapByteBuffer(capacity, capacity);
          }
      
          HeapByteBuffer(int cap, int lim) {            // package-private
      
              super(-1, 0, lim, cap, new byte[cap], 0);
              /*
              hb = new byte[cap];
              offset = 0;
              */
          }
      
    • 该方法是直接new HeapByteBuffer 对象,在堆内存中直接申请字节数组内存空间用于存储数据。

      • 直接在 JVM 堆上进行内存的分配, 本质上是 byte[] 数组的封装.
      • 但是在每次使用时,都会设计到copy操作,性能会低下。

    • ByteBuffer.allocateDirect(1024)创建堆外内存。

    • // Allocates a new direct byte buffer. 分配一个新的直接字节缓冲区
      public static ByteBuffer allocateDirect(int capacity) {
              return new DirectByteBuffer(capacity);
          }
      
          DirectByteBuffer(int cap) {                   // package-private
      
              super(-1, 0, cap, cap);
              boolean pa = VM.isDirectMemoryPageAligned(); // 《1》
              int ps = Bits.pageSize();
              long size = Math.max(1L, (long)cap + (pa ? ps : 0));
              Bits.reserveMemory(size, cap);
      
              long base = 0;
              try {
                  base = unsafe.allocateMemory(size); // 《2》
              } catch (OutOfMemoryError x) {
                  Bits.unreserveMemory(size, cap);
                  throw x;
              }
              unsafe.setMemory(base, size, (byte) 0);
              if (pa && (base % ps != 0)) {
                  // Round up to page boundary
                  address = base + ps - (base & (ps - 1));
              } else {
                  address = base; // 《3》
              }
              cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
              att = null;
          }
      
    • 从源码中看出,其实都是用的NEW关键字,宏观角度上两种方式创建的对象都是在堆内存中的。但是new DirectByteBuffer(capacity)则是基于堆外内存(直接内存 Direct)。在上述源码中导入的包设计到

      import sun.misc.Cleaner;
      import sun.misc.Unsafe;
      import sun.misc.VM;
      

      从这个角度也可以看出,这些以sun开头的类(JDK中为本地方法,非开源的。)

    • 《1》处,VM.isDirectMemoryPageAligned()本地方法的调用。

    • 《2》处:调用Unsafe方法用于分配内存。unsafe.setMemory(base, size, (byte) 0)设置内存。(这些方法都是native 本地方法。)

    • 《3》处:将分配到的内存地址 映射到该标记。(该标记为底层父类Buffer 中维护的一个成员变量 long address --->因为在堆外内存中生成的数据,必须有个映射地址,不然JVM 并不能找到该对象,因为堆外内存并不受JVM管理。)

      • // Used only by direct buffers 只适用于直接缓冲区
        // NOTE: hoisted here for speed in JNI GetDirectBufferAddress ->  static native long getDirectBufferAddress(Buffer var0);
        // 为了提高速度,将其悬挂在JNI GetDirectBufferAddress中
        long address;
        

    图解Direct Memory/Non Direct Memory

    具体的堆外内存映射关系
    • 上图所示:提到两个问题
      • JVM管理内的堆内存中的对象具体是怎么进行I/O操作的。
      • 为何要引入这种机制,使用堆外内存呢?
      • 那么在ByteBuffer创建的堆外内存对象是否被JVM管理呢?GC是否会回收该类对象呢?

    问题

    JVM管理内的堆内存中的对象具体是怎么进行I/O操作的。

    ​ 当我们使用创建对象时,大多是new出来的对象都是存放在堆内存中的,受jvm管理。受GC的管理。

    当对内存中的对象进行I/O操作时,JVM会将堆内中的对象数据完整的copy一份到堆外内存(物理内存)中,再由该物理内存中的对象进行具体的I/O操作。

    这样一来,在堆内的对象或者数据需要进行I/O操作时,都需要进行一步copy操作。(这里就引入了 NIO中的领copy操作了。后续详解。)

    为何要引入这种机制,使用堆外内存呢?

    ​ 就是为了性能。

    1. 使用堆外内存,减少了垃圾回收机制(GC会暂停其他的工作)
    2. 加快了I/O操作的进度
      1. 堆内在flush到远程时,会先复制到直接内存中,然后在发送。
      2. 而堆外内存(本身就是物理机内存)几乎省略了这步。

    那么在ByteBuffer创建的堆外内存对象是否被JVM管理呢?GC是否会回收该类对象呢?

    ​ 使用ByteBuffer创建的直接缓冲对象实际上是受JVM管理的。其他使用Unsafe创建的堆外内存对象则完全由自己控制。

    ByteBuffer allocateDirect = ByteBuffer.allocateDirect(1024);
    

    当这段代码执行会在堆外内存中占用1k的内存,Java堆内只会占用一个对象的指针引用大小。(顶层父类中维护的成员变量 address

        // Used only by direct buffers
        // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
        long address;
    

    堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。(物理内存可以扩展到很大很大。这里提及到的只是极端情况。)

    *DirectByteBuffer**分配出去的内存其实也是由**GC**负责回收的,而不像**Unsafe**是完全自行管理的***,Hotspot在GC时会扫描DirectByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。

    使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。

    堆外内存的好处

    1. 可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

    2. 理论上能减少GC暂停时间;

    3. 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

    4. 它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

    2、零拷贝 zero copy

    ​ 上面探讨的所有内容,其实已经完整的带出了零拷贝。

    ByteBuffer创建的直接缓冲区就是利用零拷贝,来提高性能的。

    堆外内存中的数据进行I/O操作时,不用将数据拷贝到堆外内存中去,所以就节省了一次拷贝操作(不用进行拷贝操作),所以成为零拷贝。

    Netty 充分的利用此种操作,用来大大的提升了性能与速度。(高性能)


    3、内存映射 MappedByteBuffer

    ​ 用于直接内存映射操作。深入浅出MappedByteBuffer

    4、Selector 选择器源码解析

    深入浅出NIO之Selector实现原理

    //TODO
    

    JNI(Java Native Interface)

    引用:

    JAVA堆内内存、堆外内存

    本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!

    转载请注明出处!

    欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。


    qrcode.jpg

    ——努力努力再努力xLg

    加油!

    相关文章

      网友评论

          本文标题:Netty 源码分析 —— NIO 基础(五)之零拷贝与其它源码

          本文链接:https://www.haomeiwen.com/subject/jtusfhtx.html