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

2020-02-14  本文已影响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();
    }
}

图解Direct Memory/Non Direct Memory

具体的堆外内存映射关系

问题

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

加油!

上一篇下一篇

猜你喜欢

热点阅读