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)这个线程始终在后盾启动,如果有援用,就会唤醒该线程。