1.什么是DMA

2.什么是用户态和内核态

3.一般BIO的拷贝流程剖析

4.mmap零碎函数

5.sendFile零碎函数(零拷贝)

6.java堆外内存如何回收

1.什么是DMA

DMA(Direct Memory Access间接存储器拜访),咱们先从一张图来理解一下DMA是一个什么安装。

假如在什么没有DMA的状况下,如果CPU想从内存里读取数据并发送到网卡中,在读的过程中,咱们能够晓得:
1.1)CPU的速度最快
1.2)当CPU在内存中读取数据的时候,读取的速度瓶颈在于内存的读写速度
1.3)当CPU实现读取,将数据写入网卡的时候,写入的速度瓶颈在于网卡的速度
1.4)CPU在读写的时候,是无奈做其它事件的。

这个时候咱们就能够得出结论:

1.5)cpu的速度取决于这一系列操作环上最慢的那一个。
1.6)cpu利用率极低,大部分工夫都在期待IO

此时如果有了DMA,那么咱们的读写就会变得和如图一样:

CPU只须要把读写工作委托给DMA,帮助CPU搬运数据,这些操作都由DMA主动执行,而不须要依赖于CPU的大量中断负载,此时cpu就能够去做其它的事件了。

2.什么是用户态和内核态
其实最初咱们的服务器程序,都是要在linux上运行的,Linux依据命令的重要水平,也分为不同的权限。Linux操作系统就将权限分成了2个等级,别离就是用户态和内核态

用户态:
用户态的过程可能拜访的资源就有极大的限度,有些指令操作无关痛痒,随便执行都没事,大部分都是属于用户态的。

内核态:
运行在内核态的过程能够“随心所欲”,能够间接调用操作系统内核函数。

比方:咱们调用malloc函数申请动态内存,此时cpu就要从用户态切换到内核态,调用操作系统底层函数来申请空间。

3.一般BIO的拷贝流程剖析

咱们来看一下一般IO的拷贝流程

咱们来看这一段代码:

咱们先从服务器上读取了一个文件,而后通过连贯和流传输到申请客户端上,咱们能够看到大抵的申请流程是这样的:

当程序或者操作者对CPU收回指令,这些指令和数据暂存在内存里,在CPU闲暇时传送给CPU,CPU解决后把后果输入到输出设备上

3.1)用户态程序接到申请,要从磁盘上读取文件,切换到内核态,这里是第1次用户态内核态切换
3.2)当要读取的文件通过DMA复制到内核缓冲区的时候,咱们还要把这些数据传送给CPU,CPU之后再把这些数据送到输出设备上,这里是第1次cpu拷贝
3.3)当内核态程序数据读取结束,切换回用户态,这里是第2次内核态用户态切换
3.4)当程序创立一个缓冲区,并将数据写入socket缓冲区,这里是第3次用户态内核态切换
3.5)此时cpu要把数据拷贝到socket缓冲区,这里是第2次cpu拷贝
3.6)实现所有操作之后,应用程序从内核态切换回用户态,继续执行后续操作(程序到此为止)。这里是第4次用户态内核态切换

此时咱们能够看出,传统的IO拷贝流程,经验了4次用户态和内核态的切换,进行了2次cpu复制,性能消耗微小,咱们有没有更节俭资源的做法呢?

4.mmap零碎函数

linux的底层内核函数mmap函数对底层进行了一个优化:

4.1)用户态程序接到申请,要从磁盘上读取文件,切换到内核态,这里是第1次用户态内核态切换
4.2)当要读取的文件通过DMA复制到内核缓冲区实现,此时内核缓冲区,用户数据缓冲区共享一块物理内存空间,这里就无需cpu拷贝到用户空间中
4.3)此时读取文件结束,用户切换回用户态,这是第2次用户态内核态切换
4.4)申请一块缓冲区,须要调用内核函数,这是第3次用户态内核态切换
4.5)内核态通过cpu复制,将共享空间的数据内容拷贝到socket缓冲区中,这是第1次cpu拷贝
4.6)实现所有操作之后,应用程序从内核态切换回用户态,继续执行后续操作(程序到此为止)。这里是第4次用户态内核态切换

咱们能够看出,mmap函数少了一次cpu复制,对于空间的利用率进步了,不过还是须要4次用户态和内核态的切换

5.sendFile零碎函数(零拷贝)

零拷贝:指的是没有cpu拷贝,数据还是须要通过DMA拷贝到内存中,再发送进来的


4.1)用户态程序接到申请,要从磁盘上读取文件,切换到内核态,这里是第1次用户态内核态切换。
4.2)当数据通过DMA复制进入内核缓冲区并且实现,咱们还是通过cpu复制把数据复制到socket缓冲区,不过这里的cpu复制只复制很大量的内容,能够简直忽略不计。

4.3)此时数据通过DMA复制发送给目的地。
4.4)程序切换回用户态,这是第2次用户态内核态切换

咱们发现,sendFile零碎函数,只须要两次用户态到内核态的切换,而且一次cpu复制都不须要,大大节约了资源。

6.java堆外内存如何回收

介绍了零拷贝技术,其实Netty底层是应用堆外内存来实现零拷贝技术的,api:ByteBuffer.allocateDirect(),这条命令间接在堆外内存开拓了一块空间,咱们都晓得GC是收集堆内存垃圾的,那堆外内存又是如何收集的呢

堆外内存的劣势:
堆外内存的劣势在于IO上,java在应用socket发送数据的时候,如果应用堆外内存,就能够间接应用堆外内存往socket上发送数据,就节俭了先把堆外数据拷贝到堆内数据的开销。

咱们先来看看ByteBuffer.allocateDirect()的源码:

咱们能够看出,java应用unsafe类来调配了一块堆外内存

那么堆外内存是如何回收的呢?咱们来看这样一行代码:

cleaner就是用来回收堆外内存的,然而它是如何工作的呢?咱们认真钻研一下cleaner这个类,它是一个链表构造:

通过create(Object,Runnable)办法创立cleaner对象,调用本身的add办法,将其退出链表中。

clean有个重要的clean办法, 它首先将对象从本身链表中删除:

而后执行this.thunk的run办法,thunk就是由创立的时候传入的Runnable函数:

能够看出,run办法是一个开释堆外内存的函数。

逻辑咱们曾经梳理完,然而JVM如何开释其占用的堆外内存呢如何跟Cleaner关联起来

首先,Cleaner继承了PhantomReference(虚援用),对于强脆弱虚援用,在后面的博客曾经赘述过:深刻了解JVM(八)——强脆弱虚援用

简略地再介绍一下虚援用,当GC某个对象的时候,如果此对象上有虚援用,会将其退出PhantomReference退出到ReferenceQueue队列。

Cleaner继承PhantomReference,而PhantomReference又继承Reference,Reference初始化的时候,会运行一个动态代码块:

咱们能够看出,ReferenceHandler作为一个优先级比拟高的守护线程被启动了。

在看他的解决逻辑之前,咱们先理解一下对象的四种状态;

  • Active:激活。创立ref对象时就是激活状态
  • Pending:期待入援用队列。所对应的援用被GC,就要入队。
  • Enqueued:入队状态。

    • 如果指定了refQueue生产pending挪动到enqueued状态。refQueue.poll时进入生效状态
    • 如果没有指定refQueue,间接到生效状态。
  • Inactive:生效

接下来咱们能够看业务逻辑了:

这是一个死循环,咱们再往里点:

    static boolean tryHandlePending(boolean waitForNotify) {        Reference<Object> r;        Cleaner c;        try {            //可能有多线程对一个援用队列操作,所以要加锁            synchronized (lock) {                  //如果以后对象是 期待入援用队列 的状态                if (pending != null) {                    r = pending;                    // 'instanceof' might throw OutOfMemoryError sometimes                    // so do this before un-linking 'r' from the 'pending' chain...                    //转化为clean对象                    c = r instanceof Cleaner ? (Cleaner) r : null;                    // unlink 'r' from 'pending' chain                    //解除援用                    pending = r.discovered;                    r.discovered = null;                } else {                     //如果没有,期待唤醒                    // The waiting on the lock may cause an OutOfMemoryError                    // because it may try to allocate exception objects.                    if (waitForNotify) {                        lock.wait();                    }                    // retry if waited                    return waitForNotify;                }            }        } catch (OutOfMemoryError x) {            // Give other threads CPU time so they hopefully drop some live references            // and GC reclaims some space.            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above            // persistently throws OOME for some time...            Thread.yield();            // retry            return true;        } catch (InterruptedException x) {            // retry            return true;        }        // Fast path for cleaners        //革除内存        if (c != null) {            c.clean();            return true;        }        ReferenceQueue<? super Object> q = r.queue;        if (q != ReferenceQueue.NULL) q.enqueue(r);        return true;    }

咱们能够得出:
1)当对象状态是Pending的时候,就会进入if,将这个对象转化为clean对象,并将这个援用置空
2)进行clean的垃圾收集
3)这个线程始终在后盾启动,如果有援用,就会唤醒该线程。