2024年Java最新零拷贝技术在Java中为何这么牛?,史上最全的Java面试题集锦在这里

最后总结我的面试经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

BAT面试经验

实战系列:Spring全家桶+Redis等

其他相关的电子书:源码+调优

面试真题:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

好了,了解完 mmap 内存映射的原理后,我们再来看下 mmap 是怎么对上面传统 IO 进行优化的。

在这里插入图片描述

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。

如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 socket 缓冲区。

现在,你只需要从内核缓冲区拷贝到 socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。

2、sendfile

那么,我们还能继续优化吗? Linux 2.1 版本 提供了 sendFile 函数。

when calling the sendfile() system call, data are fetched from disk and copied into a kernel buffer by DMA copy. Then data are copied directly from the kernel buffer to the socket buffer. Once all data are copied into the socket buffer, the sendfile() system call will return to indicate the completion of data transfer from the kernel buffer to socket buffer. Then, data will be copied to the buffer on the network card and transferred to the network.

其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。

在这里插入图片描述

如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用 write 方法时,从内核缓冲区进入到 socket,这时,是没有上下文切换的,因为都在内核空间。

最后,数据从 socket 缓冲区进入到协议栈。此时,数据经过了 3 次拷贝,2 次上下文切换。那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?

3、Sendfile With DMA Scatter/Gather Copy

实际上,Linux 在 2.4 版本中,做了一些优化。

Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.

避免了从内核缓冲区拷贝到 socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

具体如下图:

在这里插入图片描述

Scatter/Gather 可以看作是 sendfile 的增强版,批量 sendfile。

现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 socket buffer,基本无消耗。

4、零拷贝小结

等一等,老周,不是说的零拷贝吗?怎么还需要 2 次拷贝?

首先我们说零拷贝,是从操作系统的角度来说的(也就是我们上文所说的狭义零拷贝)。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝(严谨点的话叫狭义零拷贝))。例如我们刚开始的例子,内核缓存区和 socket 缓冲区的数据就是重复的。

mmap 和 sendFile 的区别

  • mmap 适合小数据量读写,sendFile 适合大文件传输。

  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

  • RocketMQ 在消费消息时,使用了 mmap。Kafka 使用了 sendFile。

| | CPU拷贝 | DMA拷贝 | 上下文切换 | 系统调用 |

| — | — | — | — | — |

| 传统 IO | 2 | 2 | 4 | read/write |

| mmap | 1 | 2 | 4 | mmap/write |

| sendfile | 1 | 2 | 2 | sendfile |

| sendfile with dma scatter/gather copy | 0 | 2 | 2 | sendfile |

四、零拷贝在Java中的应用


1、NIO

1.1 MappedByteBuffer

首先要说明的是,Java NlO 中 的 Channel (通道)就相当于操作系统中的内核缓冲区,有可能是读缓冲区,也有可能是网络缓冲区,而 Buffer 就相当于操作系统中的用户缓冲区。

MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, “r”)

.getChannel()

.map(FileChannel.MapMode.READ_ONLY, 0, len);

NIO 中的 FileChannel.map() 方法其实就是采用了操作系统中的内存映射方式,底层就是调用 Linux mmap() 实现的。

将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过 SocketChannel 发送,还是需要CPU进行数据的拷贝。

使用 MappedByteBuffer,小文件,效率不高;一个进程访问,效率也不高。

MappedByteBuffer 只能通过调用 FileChannel 的 map() 取得,再没有其他方式。

FileChannel.map() 是抽象方法,具体实现是在 FileChannelImpl.map() 可自行查看 JDK 源码,其 map0() 方法就是调用了 Linux 内核的 mmap 的 API。

使用 MappedByteBuffer 类要注意的是:mmap的文件映射,在 full gc 时才会进行释放。当 close 时,需要手动清除内存映射文件,可以反射调用 sun.misc.Cleaner 方法。

1.2 sendfile

  • FileChannel.transferTo() 方法直接将当前通道内容传输到另一个通道,没有涉及到 Buffer 的任何操作,NIO 中的 Buffer 是 JVM 堆或者堆外内存,但不论如何他们都是操作系统内核空间的内存。

  • transferTo() 的实现方式就是通过系统调用 sendfile() (当然这是Linux中的系统调用)。

// 使用sendfile:读取磁盘文件,并网络发送

FileChannel sourceChannel = new RandomAccessFile(source, “rw”).getChannel();

SocketChannel socketChannel = SocketChannel.open(sa);

sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);

ZeroCopyFile实现文件复制:

class ZeroCopyFile {

public void copyFile(File src, File dest) {

try (FileChannel srcChannel = new FileInputStream(src).getChannel();

FileChannel destChannel = new FileInputStream(dest).getChannel()) {

srcChannel.transferTo(0, srcChannel.size(), destChannel);

} catch (IOException e) {

e.printStackTrace();

}

}

}

Java NIO 提供的 FileChannel.transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。

2、Netty

Netty 中也用到了 FileChannel.transferTo 方法,所以 Netty 的零拷贝也包括上面讲的操作系统级别的零拷贝。

传统的 ByteBuffer,如果需要将两个 ByteBuffer 中的数据组合到一起,我们需要首先创建一个size=size1+size2 大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用 Netty 提供的组合 ByteBuf,就可以避免这样的操作,因为 CompositeByteBuf 并没有真正将多个 Buffer 组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。

CompositeByteBuf:将多个缓冲区显示为单个合并缓冲区的虚拟缓冲区。

建议使用 ByteBufAllocator.compositeBuffer() 或者 Unpooled.wrappedBuffer(ByteBuf…),而不是显式调用构造函数。

我们l来看下源码

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {

public static final ByteToMessageDecoder.Cumulator MERGE_CUMULATOR = new ByteToMessageDecoder.Cumulator() {

public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {

ByteBuf var5;

try {

ByteBuf buffer;

if (cumulation.writerIndex() <= cumulation.maxCapacity() - in.readableBytes() && cumulation.refCnt() <= 1 && !cumulation.isReadOnly()) {

buffer = cumulation;

} else {

buffer = ByteToMessageDecoder.expandCumulation(alloc, cumulation, in.readableBytes());

}

buffer.writeBytes(in);

var5 = buffer;

} finally {

in.release();

}

return var5;

}

};

// 可以看出来这里用了ByteBufAllocator 来分配readable的空间,并写入累积器中

static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {

ByteBuf oldCumulation = cumulation;

cumulation = alloc.buffer(cumulation.readableBytes() + readable);

cumulation.writeBytes(oldCumulation); // 将原始累积器的数据copy到新的累积器

oldCumulation.release(); // 释放原始的累积器

return cumulation;

}

}

写文件Region

从这里我们可以看出 netty 也调用了 FileChannelDe tansferTo 方法:

public class DefaultFileRegion extends AbstractReferenceCounted implements FileRegion {

private FileChannel file;

public long transferTo(WritableByteChannel target, long position) throws IOException {

long count = this.count - position;

if (count >= 0L && position >= 0L) {

if (count == 0L) {

return 0L;

} else if (this.refCnt() == 0) {

throw new IllegalReferenceCountException(0);

} else {

this.open();

long written = this.file.transferTo(this.position + position, count, target);

if (written > 0L) {

this.transferred += written;

} else if (written == 0L) {

validate(this, position);

}

return written;

}

} else {

throw new IllegalArgumentException("position out of range: " + position + " (expected: 0 - " + (this.count - 1L) + ‘)’);

}

}

}

3、RocketMQ、Kafka等MQ

MQ这块的应用老周后续再来分析,敬请期待~


最后

整理的这些资料希望对Java开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

image

image

其实面试这一块早在第一个说的25大面试专题就全都有的。以上提及的这些全部的面试+学习的各种笔记资料,我这差不多来回搞了三个多月,收集整理真的很不容易,其中还有很多自己的一些知识总结。正是因为很麻烦,所以对以上这些学习复习资料感兴趣,

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

[外链图片转存中…(img-bHsreMKw-1714994552092)]

[外链图片转存中…(img-RdUPcs8i-1714994552092)]

其实面试这一块早在第一个说的25大面试专题就全都有的。以上提及的这些全部的面试+学习的各种笔记资料,我这差不多来回搞了三个多月,收集整理真的很不容易,其中还有很多自己的一些知识总结。正是因为很麻烦,所以对以上这些学习复习资料感兴趣,

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值