零拷贝、NIO、内核态、DMA、Java堆外内存

零拷贝、NIO、内核态、DMA、Java堆外内存

零拷贝在很多中间件里面都有广泛的应用;正是因为有了零拷贝的存在,才会出现想Kafka、RocketMQ、Netty这种高性能、高吞吐量的网络中间件;所以此片文章就是从最底层硬件开始,讲解零拷贝的实现原理;保证清晰


DMA

想要搞明白一个东西;我们只需要明白俩个问题:是什么;为什么!

  • 什么是DMA?

DMA全称Direct Memory Access(直接访问存储器)。这是指一种高速的数据传输操作,允许在外部设备和存储器之间直接读写数据

  • 为什么要有DMA?

首先看看不使用DMA的情况;怎么取数据发数据?

CPU 需要从内存中读取数据到自身的高速缓存;然后再发到网卡中;整个过程都需要CPU来进行数据的拷贝;这也就意味着再此期间CPU不能处理其他进程的请求;

DMA存在:

cpu将数据读取到自身缓存;然后复制到内核socket缓冲区;随后给DMA发送指令让它进行数据转移到网卡;

其实就是相当于java将消息放到中间件然后启用一个小弟将数据进行转移,自己就可以去干其他事情了;

然后呢:DMA将数据拷贝完毕之后,就得通知CPU让他来干活了;也就是中断

这样有一来,CPU只用专注于处理线程的高计算任务,整个系统的性能就有了质的飞跃!

用户空间:

用户调用库函数完成系统调用;

用户态切换到内核态;read;中断切换; int 80中断

每一个中断都有对应的系统处理程序;

处理程序去本地磁盘加载;加载到内核缓冲区;加载完毕之后;吧数据拷贝到进程空间里面区;内核态下的进程权重很高;

切换到用户态;恢复现场;数据就已经拿到了;

内核空间:

用户进程想要拿本地数据都得依靠系统调用;内核程序检查缓冲区有没有数据;有就直接拷贝到用户空间;如果没有的话就将加载数据的操作交给DMA去做;异步把数据加载到内核缓冲区;完毕之后CPU接受中断;唤醒对应得线程区拿数据;

写数据:

缓冲区有大小限制;如果写数据的线程写满了的话会被阻塞住;等DMA把数据从缓冲区发送到网卡;给CPU中断后CPU 就会唤醒阻塞的线程!线程又可以继续写数据了;

其中缓冲区对应一个等待队列,供CPU唤醒和阻塞使用!

物理内存:

所谓内存,无非就是保存数据,物理内存就是保存一堆的0101呗;

可以这样想,程序中如何保存数据;最简单的方式就是定义一个数组;往数组的索引存放数据罢了;

其实物理内存也不过如此;就是一个大一点的数组罢了;

字节数组;又长又大;访问很快;为进程提供不同的区间进程存储数据;

虚拟内存:

早期物理内存一般都很小,所以开不了太多进程;那如何解决呢;伟大的工程师们抽象出来一个虚拟内存的概念;作用就是每个进程都有自己的一块虚拟的内存空间,这些空间地址终归映射到内存的实际物理地址上;但是因为有了这个虚拟内存之后;多个进程就可以动态的占用同一块物理地址;无非就是数据的换入换出;

虚拟内存大于物理对象;虚拟内存最终要映射到物理内存;OS负责;MMU单元通过将虚拟内存翻译成物理内存;

如果内存有1G,虚拟内存有2G,超出的部分如何处理?

OS通过LRU或LFU算法将不常访问的空间程序转移到磁盘上的swap区域;访问的时候再转移回来就可以完成虚拟内存的映射;

有了虚拟内存,各个进程的虚拟地址尽管不同;但是映射到物理内存的地址是可以相同的!

有了这个理念;也就是说内核指向的物理内存可以和用户指向的物理内存相同;所以,只要我们划分出一块区域;用户态和内核态虚拟地址的映射都一样,那不就用户态可以访问内核态的地址空间了?所以用户态需要读取数据的话,还用切换吗?那不就减少数据的拷贝了?

流程:我的Java程序需要读取磁盘上的文件;步骤就会变成下面几步;

  1. CPU去内核找缓冲区,有没有数据;有就直接让java程序读;
  2. 没有就让小弟DMA去读取一下;将数据转移到内核空间一块地址;这块地址通过虚拟映射到物理内存;
  3. DMA完成读取,调用中断程序中断CPU,CPU直接唤醒之前挂起的进程,进程直接读取映射的物理地址就可以拿到数据!

对比传统IO :

  1. DMA : 磁盘 ------> 内核buffer; 拷贝一次
  2. CPU :内核Buffer ------ > 用户DataBuffer : 拷贝 * 2
  3. CPU : 用户DataBuffer -------> socket Buffer ; 拷贝 * 3
  4. DMA :socketBuffer ------->网卡; 拷贝*4;

总计:四次拷贝;四次用户态内核态切换;

MMap

数据从磁盘拷贝到内核缓冲区;随后就直接拷贝到socket缓冲区,下一步就可以发给网卡进行网路传输了!减少了向用户空间缓冲去复制的一次拷贝!

SendFile

函数直接将数据从内核缓存区发送到(拷贝)socket缓冲区即可;少了一些没必要的切换;一步到位;

其实就是把read和write合并成一个了!

BIO

write 、 accept阻塞;

Java OutPut对应一个缓冲去;用来写入数据;写满之后通知CPU进行数据拷贝到socket缓冲区;这个阶段是属于系统调用的;涉及到用户态和内核态的切换;

NIO:

面向块传输:

channel:通道;底层支持还是socket;

在BIO中;output对应的buffer满了才会系统调用然后发送到socketbuffer;这会导致触发多次的系统调用;所以在NIO的模式下,取消了这种传输方式,程序只需要将需要发送的数据放到连续的内存地址,组成一个块;这样一来,只需要将起始地址和长度发给内核处理程序;就可以一次性的将数据COPY到socket;因为内核程序是可以随便访问空间地址数据的;

仔细观察就会发现;要实现NIO的前提是:起始位置和长度一定要准确,且在拷贝没完成期间该地址空间不能发生变化;有没有发生变化的可能呢?

首先,Java程序数据的位置在哪,堆内存,没错吧!也就是在JVM中;那么也就是有GC的存在;所以!数据的位置会发生变化嘛?

熟悉GC的都知道,垃圾回收算法中有一个整理算法,也就是将内存碎片进行整理;如果在内核进程进程拷贝的过程中发生了GC,STW,数据的位置就会发生变化!

所以JVM选择如何解决这个问题呢?-------堆外内存

过程

  1. 将JVM堆内存的数据块拷贝到对应的堆外内存----CPU copy
  2. 发起write请求;传位置信息和长度信息
  3. 内核拷贝数据到socket;
  4. DMA 发送数据到网卡

堆外内存DirectByteBuffer

JVM将数据交给堆外管理之后,自己就没有管理权限了;那么也就意味着;这块空间的占用如何释放?这是个问题!

在Java中存在四种引用;正是因为这种机制的存在;才得以在某些操作下进行特定操作!

  • Java是如何分配堆外内存的?

那当然是看源码;源码中DirectByteBuffer构造方法有这么一行代码

点进去一看会发现是个native方法;具体实现就是JVM调用C++代码向OS分配空间!

然后特别留意一行代码;初始DirectByteBuffer的时候也初始化了一个cleaner对象!

那么这个Deallocator对象是干嘛的?

可以预见的是,该对象实现了线程;最终会执行run方法;在run方法中就调用usafe进行内存的释放!

那么我们如何知道这个cleaner对象的调用时机呢?这就不得不看看这个对象的具体结构了!

  • Cleaner对象

可以发现;该对象继承了虚引用!那么为什么要继承虚引用呢?有什么特别的地方吗

这就要先聊一聊Java的几种引用了!

Java 引用队列

Java中有四种类型的引用

重点看虚引用----PhantomReference

get方法永远为空;

主要看构造方法:需要传一个引用对象!还有一个引用队列;具体有什么用,请看下文

Reference对象有四种状态:

此Reference对象可能会有四种状态:active, pending, enqueued, inactiveavtive: 新创建的对象状态是active

pending: 当Reference所指向的对象不可达,并且Reference与一个引用队列关联,那么垃圾收集器

会将Reference标记为pending,并且会将之加到pending队列里面

enqueued: 当Reference从pending队列中,移到引用队列中之后,就是enqueued状态

inactive: 如果Reference所指向的对象不可达,并且Reference没有与引用队列关联,Reference

从引用队列移除之后,变为inactive状态。inactive就是最终状态

  • pending队列谁来消费?

看源码会发现;reference对象在实例化的时候也初始化了一块静态代码块:

初始化一个handler,启动一个守护线程在后台一直运行!然后执行start方法,这一看就是线程;点进去一看

最终执行run方法;继续点击

判断pending出来的对象是不是cleaner类型;如果是的话;使用头插法构建cleaner链表!将在下文会用到;

c != null;调用清理方法清理堆外内存;

调用thunk线程的run方法,这个线程又是刚开始初始化的;也就是

Deallocator线程!

他的run方法也就是

所以,最终调用usafe清理了堆外内存!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值