Java sendFile 底层实现
前言
Java send file api 是 transferTo 方法和 transferFrom 方法。
注意:send file 是一个从磁盘到网卡驱动的 IO 优化。反过来,网卡到磁盘,是没有这个 IO 优化的。也就是说 transferFrom 方法并没有这种福利。
本文将稍稍深入,看看 Java 如何实现,注意,本文代码版本为 openjdk-8u40-src-b25-10_feb_2015
。
transferFrom 解析
那么 transferFrom 底层是什么呢?简单说,就是使用了 MMAP 和 堆外内存而已。
上面的 transferFrom 方法代码中:
如果是普通 FileChannel 的话,就走 mmap,否则,走堆外内存,例如我们本次使用的 SocketChannelImpl。
transferFrom by mmap 细节:
简单来讲,在一个循环中,每次就是将 源文件根据 position 映射为一个 mmap,最大 8M,逐次的将数据写入的目标文件中。
transferFrom by 堆外内存细节:
从上面的代码可知,
如果我们使用的是 SocketChannelImpl ,就会走堆外内存,也是在一个循环里进行写入,每次最大 8k。用完尽量进行回收,不是释放。
其中,获取堆外内存的方法:
这个 bufferCache 是一个 ThreadLocal, 如下图所示 ,线程安全。类 netty 内存设计。
注意 ,这个 bufferCache 是 sun 写的一个简单版本的 基于直接内存的 Cache,是一个简单的内存池实现。内部是个数组,默认大小 16。get 方法的 key 是 size,即,如果数组中,有 capacity 超过 size ,就返回这个 buffer。
16 的来源是 JVM 底层实现,具体位置:IOUtil.c 140 line。
现在,假设这个 cache 有16 个槽位,内部现在有 14 个 ByteBuffer,此时 count 是 14。
start 指针指向头部,执行 get 方法,此时我们从 start 开始进行遍历,找到了下标位于 i 的元素,因为他的 capacity 比给定的 size 大。我们需要拿出这个元素。
此时 count 需要减去1,变成 13,同时,将头部的 start 元素移动到刚刚空出的位置。
注意,同时我们还需要将 start 指针放置于头部位置:
执行 start = (start + 1) % 16
, 下次再次get 的时候,还是从头部开始。为什么这么做?为了避免无谓的遍历,当遇到空元素时,就直接 return 就好了。
具体参见代码:
ByteBuffer bb = buffers[i];
if (bb == null)// 避免无谓循环
break;
当用完这个 ByteBuf 之后,就需要进行归还————如果 cache 里还有空余空间,即 count < 16。
如何归还?
有个 Util.releaseTemporaryDirectBuffer(bb)
方法, 该方法用于归还内存,注意:如果内存池满了,就调用 free 方法进行内存释放。
最终调用 offerFirstTemporaryDirectBuffer 方法。
如果添加进缓存池失败,就 free,添加缓存池的逻辑:
释放逻辑,简单来说,就是调用 unsafe 的 freeMemory 方法
限于篇幅,下次再说,还涉及到一些虚引用的内容。
transferFrom 方法小结
- 如果是源是 FileChannelImpl 类型, 就走 mmap ,循环映射 8MB 刷进磁盘。
- 如果源是 SocketChannelImpl 类型,就走堆外内存。简单来说,就是循环放进堆外内存,每次 8kb 刷进磁盘。注意:关于这个堆外内存,是用到了缓存池子的(堆外内存池化是常用优化手段),这个池子是个数组,长度是 16,使用 ThreadLocal 提升性能,每次获取,只要目标数组比池子中的 ByteBuffer 的 capacity 小即可使用,用完就还,如果满了,就调用 unsafe 释放。
transferTo 解析
transferTo 方法很有意思,先简单说下结论:
- 如果 OS 支持 send file(windows 不支持),就执行 system call。
- 如果 OS 不支持,就走 mmap。
- 如果 mmap 失败,就走 堆外内存。
代码如上。
注意:如果内核无法执行,返回 -2。在 jvm 代码中看到,apple ,linux,solaris, 还有 IBM 的 AIX 都支持 send file。
以上代码位置:FileChannelImpl.c 156 line。
如何使用 mmap 写进网卡?和 transferFrom 类似,每次最大映射 8Mb 内存,刷进网卡。每次用完之后 clean。
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。