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。