中国大唐集团公司招聘网站,页面优化,提升学历官网,iis7.5 网站打不开作者#xff1a;ksfzhaohui 来源#xff1a;http://t.cn/ESALgwV前言从字面意思理解就是数据不需要来回的拷贝#xff0c;大大提升了系统的性能#xff1b;这个词我们也经常在java nio#xff0c;netty#xff0c;kafka#xff0c;RocketMQ等框架中听到#xff0c;经常… 作者ksfzhaohui 来源http://t.cn/ESALgwV前言从字面意思理解就是数据不需要来回的拷贝大大提升了系统的性能这个词我们也经常在java nionettykafkaRocketMQ等框架中听到经常作为其提升性能的一大亮点下面从I/O的几个概念开始进而在分析零拷贝。I/O概念1.缓冲区缓冲区是所有I/O的基础I/O讲的无非就是把数据移进或移出缓冲区进程执行I/O操作就是向操作系统发出请求让它要么把缓冲区的数据排干(写)要么填充缓冲区(读)下面看一个java进程发起read请求加载数据大致的流程图进程发起read请求之后内核接收到read请求之后会先检查内核空间中是否已经存在进程所需要的数据如果已经存在则直接把数据copy给进程的缓冲区如果没有内核随即向磁盘控制器发出命令要求从磁盘读取数据磁盘控制器把数据直接写入内核read缓冲区这一步通过DMA完成接下来就是内核将数据copy到进程的缓冲区如果进程发起write请求同样需要把用户缓冲区里面的数据copy到内核的socket缓冲区里面然后再通过DMA把数据copy到网卡中发送出去你可能觉得这样挺浪费空间的每次都需要把内核空间的数据拷贝到用户空间中所以零拷贝的出现就是为了解决这种问题的关于零拷贝提供了两种方式分别是mmapwrite方式sendfile方式。2.虚拟内存所有现代操作系统都使用虚拟内存使用虚拟的地址取代物理地址这样做的好处是1.一个以上的虚拟地址可以指向同一个物理内存地址2.虚拟内存空间可大于实际可用的物理地址利用第一条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址这样DMA就可以填充对内核和用户空间进程同时可见的缓冲区了大致如下图所示省去了内核与用户空间的往来拷贝java也利用操作系统的此特性来提升性能下面重点看看java对零拷贝都有哪些支持。3.mmapwrite方式使用mmapwrite方式代替原来的readwrite方式mmap是一种内存映射文件的方法即将一个文件或者其它对象映射到进程的地址空间实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系这样就可以省掉原来内核read缓冲区copy数据到用户缓冲区但是还是需要内核read缓冲区将数据copy到内核socket缓冲区大致如下图所示4.sendfile方式sendfile系统调用在内核版本2.1中被引入目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile系统调用的引入不仅减少了数据复制还减少了上下文切换的次数大致如下图所示数据传送只发生在内核空间所以减少了一次上下文切换但是还是存在一次copy能不能把这一次copy也省略掉Linux2.4内核中做了改进将Kernel buffer中对应的数据描述信息内存地址偏移量记录到相应的socket缓冲区当中这样连内核空间中的一次cpu copy也省掉了。Java零拷贝1.MappedByteBufferjava nio提供的FileChannel提供了map()方法该方法可以在一个打开的文件和MappedByteBuffer之间建立一个虚拟内存映射MappedByteBuffer继承于ByteBuffer类似于一个基于内存的缓冲区只不过该对象的数据元素存储在磁盘的一个文件中调用get()方法会从磁盘中获取数据此数据反映该文件当前的内容调用put()方法会更新磁盘上的文件并且对文件做的修改对其他阅读者也是可见的下面看一个简单的读取实例然后在对MappedByteBuffer进行分析public class MappedByteBufferTest {public static void main(String[] args) throws Exception { File file new File(D://db.txt); long len file.length(); byte[] ds new byte[(int) len]; MappedByteBuffer mappedByteBuffer new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, len); for (int offset 0; offset len; offset) { byte b mappedByteBuffer.get(); ds[offset] b; } Scanner scan new Scanner(new ByteArrayInputStream(ds)).useDelimiter( ); while (scan.hasNext()) { System.out.print(scan.next() ); } }}
主要通过FileChannel提供的map()来实现映射map()方法如下 public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
分别提供了三个参数MapModePosition和size分别表示MapMode映射的模式可选项包括READ_ONLYREAD_WRITEPRIVATEPosition从哪个位置开始映射字节数的位置Size从position开始向后多少个字节重点看一下MapMode请两个分别表示只读和可读可写当然请求的映射模式受到Filechannel对象的访问权限限制如果在一个没有读权限的文件上启用READ_ONLY将抛出NonReadableChannelExceptionPRIVATE模式表示写时拷贝的映射意味着通过put()方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到该过程不会对底层文件做任何修改而且一旦缓冲区被施以垃圾收集动作garbage collected那些修改都会丢失大致浏览一下map()方法的源码 public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException{ ...省略... int pagePosition (int)(position % allocationGranularity); long mapPosition position - pagePosition; long mapSize size pagePosition; try { // If no exception was thrown from map0, the address is valid addr map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError x) { // An OutOfMemoryError may indicate that weve exhausted memory // so force gc and re-attempt map System.gc(); try { Thread.sleep(100); } catch (InterruptedException y) { Thread.currentThread().interrupt(); } try { addr map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError y) { // After a second OOME, fail throw new IOException(Map failed, y); } }// On Windows, and potentially other platforms, we need an open // file descriptor for some mapping operations. FileDescriptor mfd; try { mfd nd.duplicateForMapping(fd); } catch (IOException ioe) { unmap0(addr, mapSize); throw ioe; }assert (IOStatus.checkAll(addr)); assert (addr % allocationGranularity 0); int isize (int)size; Unmapper um new Unmapper(addr, mapSize, isize, mfd); if ((!writable) || (imode MAP_RO)) { return Util.newMappedByteBufferR(isize, addr pagePosition, mfd, um); } else { return Util.newMappedByteBuffer(isize, addr pagePosition, mfd, um); } }
大致意思就是通过native方法获取内存映射的地址如果失败手动gc再次映射最后通过内存映射的地址实例化出MappedByteBufferMappedByteBuffer本身是一个抽象类其实这里真正实例话出来的是DirectByteBuffer2.DirectByteBufferDirectByteBuffer继承于MappedByteBuffer从名字就可以猜测出开辟了一段直接的内存并不会占用jvm的内存空间上一节中通过Filechannel映射出的MappedByteBuffer其实际也是DirectByteBuffer当然除了这种方式也可以手动开辟一段空间ByteBuffer directByteBuffer ByteBuffer.allocateDirect(100);
如上开辟了100字节的直接内存空间3.Channel-to-Channel传输经常需要从一个位置将文件传输到另外一个位置FileChannel提供了transferTo()方法用来提高传输的效率首先看一个简单的实例public class ChannelTransfer { public static void main(String[] argv) throws Exception { String files[]new String[1]; files[0]D://db.txt; catFiles(Channels.newChannel(System.out), files); }private static void catFiles(WritableByteChannel target, String[] files) throws Exception { for (int i 0; i files.length; i) { FileInputStream fis new FileInputStream(files[i]); FileChannel channel fis.getChannel(); channel.transferTo(0, channel.size(), target); channel.close(); fis.close(); } }}
通过FileChannel的transferTo()方法将文件数据传输到System.out通道接口定义如下 public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
几个参数也比较好理解分别是开始传输的位置传输的字节数以及目标通道transferTo()允许将一个通道交叉连接到另一个通道而不需要一个中间缓冲区来传递数据注这里不需要中间缓冲区有两层意思第一层不需要用户空间缓冲区来拷贝内核缓冲区另外一层两个通道都有自己的内核缓冲区两个内核缓冲区也可以做到无需拷贝数据Netty零拷贝netty提供了零拷贝的buffer在传输数据时最终处理的数据会需要对单个传输的报文进行组合和拆分Nio原生的ByteBuffer无法做到netty通过提供的Composite(组合)和Slice(拆分)两种buffer来实现零拷贝看下面一张图会比较清晰TCP层HTTP报文被分成了两个ChannelBuffer这两个Buffer对我们上层的逻辑(HTTP处理)是没有意义的。但是两个ChannelBuffer被组合起来就成为了一个有意义的HTTP报文这个报文对应的ChannelBuffer才是能称之为”Message”的东西这里用到了一个词”Virtual Buffer”。可以看一下netty提供的CompositeChannelBuffer源码public class CompositeChannelBuffer extends AbstractChannelBuffer { private final ByteOrder order; private ChannelBuffer[] components; private int[] indices; private int lastAccessedComponentId; private final boolean gathering; public byte getByte(int index) { int componentId componentId(index); return components[componentId].getByte(index - indices[componentId]); } ...省略...
components用来保存的就是所有接收到的bufferindices记录每个buffer的起始位置lastAccessedComponentId记录上一次访问的ComponentIdCompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容而是直接保存了所有ChannelBuffer的引用并在子ChannelBuffer里进行读写实现了零拷贝。其他零拷贝RocketMQ的消息采用顺序写到commitlog文件然后利用consume queue文件作为索引RocketMQ采用零拷贝mmapwrite的方式来回应Consumer的请求同样kafka中存在大量的网络数据持久化到磁盘和磁盘文件通过网络发送的过程kafka使用了sendfile零拷贝方式总结零拷贝如果简单用java里面对象的概率来理解的话其实就是使用的都是对象的引用每个引用对象的地方对其改变就都能改变此对象永远只存在一份对象。【End】老王给大家准备一份「Java最常见200面试题全解析」助力大家找到更好的工作这份面试题包含的模块Java、JVM 最常见面试题解析Spring、Spring MVC、MyBatis、Hibernate 面试题解析MySQL、Redis 面试题解析RabbitMQ、Kafka、Zookeeper 面试解析微服务 Spring Boot、Spring Cloud 面试解析扫描下面二维码付费阅读关注下方二维码订阅更多精彩内容。转发朋友圈是对我最大的支持。