Java Nio FileChannel堆內(nèi)堆外數(shù)據(jù)讀寫全流程分析及使用
背景
java nio中文件讀寫不管是普通文件讀寫,還是基于mmap實(shí)現(xiàn)零拷貝,都離不開FileChannel這個(gè)類。
隨便打開RocketMQ 源碼搜索FileChannel。
就可以看到使用頻率。
圖片
kafka也是。
圖片
所以在java中文件讀寫FileChannel尤為重用。
java文件讀寫全流程
圖片
這里說的僅僅是FileChannel基于堆內(nèi)存(HeapByteBuffer)的文件讀寫。
如果是mmap或者堆外內(nèi)存,可能有些步驟會(huì)省略,相當(dāng)于有一些優(yōu)化。
- FileChannel調(diào)用read,將HeapByteBuffer拷貝到DirectByteBuffer。
- JVM在native層使用read系統(tǒng)調(diào)用進(jìn)行文件讀取, 這里需要進(jìn)行上下文切換,從用戶態(tài)進(jìn)入內(nèi)核態(tài)。
- JVM 進(jìn)程進(jìn)入虛擬文件系統(tǒng)層,查看文件數(shù)據(jù)再page cache是否緩存,如果有則直接從page cache讀取并返回到DirectByteBuffer。
- 如果請(qǐng)求文件數(shù)據(jù)不在page caceh,則進(jìn)入文件系統(tǒng)。通過塊驅(qū)動(dòng)設(shè)備進(jìn)行真正的IO,并進(jìn)行文件預(yù)讀,比如讀取的文件可能只有1-10,但是會(huì)將1-20都讀取。
- 磁盤控制器DMA將磁盤中的數(shù)據(jù)拷貝到page cache中。這里發(fā)生了一次數(shù)據(jù)拷貝(非CPU拷貝)。
- CPU將page cache數(shù)據(jù)拷貝到DirectByteBuffer,因?yàn)閜age cache屬于內(nèi)核空間,JVM進(jìn)程無法直接尋址。這里是發(fā)生第二次數(shù)據(jù)拷貝。
- JVM進(jìn)程從內(nèi)核態(tài)切換回用戶態(tài),這里如果使用的是堆內(nèi)存(HeapByteBuffer),實(shí)際還需要將堆外內(nèi)存DirectByteBuffer拷貝到堆內(nèi)存(HeapByteBuffer)。
FileChannel讀寫文件(非MMAP)
public static void main(String[] args) {
String filename = "小奏技術(shù).txt";
String content = "Hello, 小奏技術(shù).";
// 寫入文件
writeFile(filename, content);
// 讀取文件
System.out.println("Reading from file:");
readFile(filename);
}
public static void writeFile(String filename, String content) {
// 創(chuàng)建文件對(duì)象
File file = new File(filename);
// 確保文件存在
if (!file.exists()) {
try {
boolean created = file.createNewFile();
if (!created) {
System.err.println("Unable to create file: " + filename);
return;
}
} catch (Exception e) {
System.err.println("An error occurred while creating the file: " + e.getMessage());
return;
}
}
// 使用FileChannel寫入文件
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel fileChannel = randomAccessFile.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(content.getBytes().length);
buffer.put(content.getBytes());
buffer.flip(); // 切換到讀模式
while (buffer.hasRemaining()) {
fileChannel.write(buffer);
}
} catch (Exception e) {
System.err.println("An error occurred while writing to the file: " + e.getMessage());
}
}
public static void readFile(String filename) {
// 使用FileChannel讀取文件
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filename, "r");
FileChannel fileChannel = randomAccessFile.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate((int) fileChannel.size());
while (fileChannel.read(buffer) > 0) {
// Do nothing, just read
}
// 切換到讀模式
buffer.flip();
/* while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}*/
Charset charset = StandardCharsets.UTF_8;
String fileContent = charset.decode(buffer).toString();
System.out.print(fileContent);
} catch (Exception e) {
System.err.println("An error occurred while reading the file: " + e.getMessage());
}
}
這里需要注意的一個(gè)細(xì)節(jié) 我們分配的內(nèi)存的方式是:
ByteBuffer.allocate()
這里我們可以進(jìn)入看看源碼:
圖片
實(shí)際構(gòu)造的是HeapByteBuffer,也就是JVM的堆內(nèi)存。
如果我們使用:
ByteBuffer.allocateDirect()
圖片
則構(gòu)造的是堆外內(nèi)存DirectByteBuffer。
HeapByteBuffer和DirectByteBuffer文件讀寫區(qū)別
我們看看FileChannel read方法:
圖片
發(fā)現(xiàn)IO相關(guān)的處理被封裝在IOUtil,我們繼續(xù)看看IOUtil的write方法:
圖片
可以看到如果是DirectBuffer則可以直接寫。如果是HeapByteBuffer則需要轉(zhuǎn)換為DirectByteBuffer。
圖片
為什么要在DirectByteBuffer做一層轉(zhuǎn)換
主要是HeapByteBuffer受JVM管理,也就是會(huì)受到GC影響。如果在進(jìn)行native調(diào)用的時(shí)候發(fā)生了GC,會(huì)導(dǎo)致HeapByteBuffer的內(nèi)容出現(xiàn)錯(cuò)誤。具體詳細(xì)的說明可以看看這篇MappedByteBuffer VS FileChannel:從內(nèi)核層面對(duì)比兩者的性能差異。講解的非常清晰。
參考
- MappedByteBuffer VS FileChannel:從內(nèi)核層面對(duì)比兩者的性能差異