Java sendFile 底层实现

前言

Java send file api 是 transferTo 方法和 transferFrom 方法。

注意:send file 是一个从磁盘到网卡驱动的 IO 优化。反过来,网卡到磁盘,是没有这个 IO 优化的。也就是说 transferFrom 方法并没有这种福利。

本文将稍稍深入,看看 Java 如何实现,注意,本文代码版本为 openjdk-8u40-src-b25-10_feb_2015

transferFrom 解析

那么 transferFrom 底层是什么呢?简单说,就是使用了 MMAP 和 堆外内存而已。

img.png

上面的 transferFrom 方法代码中:

如果是普通 FileChannel 的话,就走 mmap,否则,走堆外内存,例如我们本次使用的 SocketChannelImpl。

transferFrom by mmap 细节:

img_1.png

简单来讲,在一个循环中,每次就是将 源文件根据 position 映射为一个 mmap,最大 8M,逐次的将数据写入的目标文件中。

transferFrom by 堆外内存细节:

img_2.png

从上面的代码可知,

如果我们使用的是 SocketChannelImpl ,就会走堆外内存,也是在一个循环里进行写入,每次最大 8k。用完尽量进行回收,不是释放。

其中,获取堆外内存的方法:

img_3.png

这个 bufferCache 是一个 ThreadLocal, 如下图所示 ,线程安全。类 netty 内存设计。

img_4.png

注意 ,这个 bufferCache 是 sun 写的一个简单版本的 基于直接内存的 Cache,是一个简单的内存池实现。内部是个数组,默认大小 16。get 方法的 key 是 size,即,如果数组中,有 capacity 超过 size ,就返回这个 buffer。

16 的来源是 JVM 底层实现,具体位置:IOUtil.c 140 line。

img_5.png

现在,假设这个 cache 有16 个槽位,内部现在有 14 个 ByteBuffer,此时 count 是 14。

img_6.png

start 指针指向头部,执行 get 方法,此时我们从 start 开始进行遍历,找到了下标位于 i 的元素,因为他的 capacity 比给定的 size 大。我们需要拿出这个元素。

img_7.png

此时 count 需要减去1,变成 13,同时,将头部的 start 元素移动到刚刚空出的位置。

注意,同时我们还需要将 start 指针放置于头部位置:

img_8.png

执行 start = (start + 1) % 16, 下次再次get 的时候,还是从头部开始。为什么这么做?为了避免无谓的遍历,当遇到空元素时,就直接 return 就好了。

具体参见代码:

1
2
3
ByteBuffer bb = buffers[i]; 
if (bb == null)// 避免无谓循环
break;

当用完这个 ByteBuf 之后,就需要进行归还————如果 cache 里还有空余空间,即 count < 16。

如何归还?

有个 Util.releaseTemporaryDirectBuffer(bb)方法, 该方法用于归还内存,注意:如果内存池满了,就调用 free 方法进行内存释放。

最终调用 offerFirstTemporaryDirectBuffer 方法。

img_9.png

如果添加进缓存池失败,就 free,添加缓存池的逻辑:

img_10.png

释放逻辑,简单来说,就是调用 unsafe 的 freeMemory 方法

img_11.png

限于篇幅,下次再说,还涉及到一些虚引用的内容。

transferFrom 方法小结

  1. 如果是源是 FileChannelImpl 类型, 就走 mmap ,循环映射 8MB 刷进磁盘。
  2. 如果源是 SocketChannelImpl 类型,就走堆外内存。简单来说,就是循环放进堆外内存,每次 8kb 刷进磁盘。注意:关于这个堆外内存,是用到了缓存池子的(堆外内存池化是常用优化手段),这个池子是个数组,长度是 16,使用 ThreadLocal 提升性能,每次获取,只要目标数组比池子中的 ByteBuffer 的 capacity 小即可使用,用完就还,如果满了,就调用 unsafe 释放。

transferTo 解析

transferTo 方法很有意思,先简单说下结论:

  1. 如果 OS 支持 send file(windows 不支持),就执行 system call。
  2. 如果 OS 不支持,就走 mmap。
  3. 如果 mmap 失败,就走 堆外内存。

img_12.png

代码如上。

注意:如果内核无法执行,返回 -2。在 jvm 代码中看到,apple ,linux,solaris, 还有 IBM 的 AIX 都支持 send file。

img_13.png

以上代码位置:FileChannelImpl.c 156 line。

如何使用 mmap 写进网卡?和 transferFrom 类似,每次最大映射 8Mb 内存,刷进网卡。每次用完之后 clean。

1
2
3
4
5
private static void unmap(MappedByteBuffer bb) {
Cleaner cl = ((DirectBuffer)bb).cleaner();
if (cl != null)
cl.clean();
}

如何使用直接内存?也和 transferFrom 类似,每次最大使用 8kb,循环刷进网卡。这里就补贴代码了。

总结

看了 send file 的 Java 层面实现,这里总结一下,只有 transferTo 用到了 send file,而且还是有条件的,具体,本文第二部分已经给出。

而 transferFrom 方法则是很普通的使用 mmap 或者 堆外内存,似乎我们有可以自己实现,反而性能可能会更好,例如我们使用更大的缓存,而不必循环多次,我们可以使用更大的 mmap 映射,而不是 8Mb,每次都需要 clean 再重新 mapping。

EOF


Java sendFile 底层实现
http://thinkinjava.cn/2019/10/30/2019/1030SFimpl/
作者
莫那·鲁道
发布于
2019年10月30日
许可协议