一即横、Java中的文件復(fù)制
1.Java IO實現(xiàn)文件復(fù)制
? ? ? ?利用java.io類庫,直接為源文件構(gòu)建一個FileInputStream讀取阵翎,然后再為目標(biāo)文件構(gòu)建一個FileOutputStream逢并,完成寫入工作。示例代碼如下:
public static void copyFileByStream(File source, File dest) throws IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
2.Java NIO實現(xiàn)文件復(fù)制
? ? ? ?利用java.nio類庫提供的transferTo或transferFrom方法實現(xiàn)郭卫。示例代碼如下:
public static void copyFileByChannel(File source, File dest) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel();){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(
sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
? ? ? ?當(dāng)然砍聊,Java標(biāo)準(zhǔn)類庫本身已經(jīng)提供了幾種Files.copy的實現(xiàn)。對于Copy的效率贰军,這個其實與操作系統(tǒng)和配置等情況相關(guān)玻蝌,總體上來說,NIO transferTo/From的方式 可能更快词疼,因為它更能利用現(xiàn)代操作系統(tǒng)底層機制俯树,避免不必要拷貝和上下文切換。
3.復(fù)制機制的分析
? ? ? ?首先贰盗,你需要理解用戶態(tài)空間(User Space)和內(nèi)核態(tài)空間(Kernel Space)许饿,這是操作系統(tǒng)層面的基本概念,操作系統(tǒng)內(nèi)核童太、硬件驅(qū)動等運行在內(nèi)核態(tài)空間米辐,具有相對高的特權(quán)胸完;而用戶態(tài)空間,則是給普通應(yīng)用和服務(wù)使用翘贮。你可以參考:https://en.wikipedia.org/wiki/User_space赊窥。當(dāng)我們使用輸入輸出流進(jìn)行讀寫時,實際上是進(jìn)行了多次上下文切換狸页,比如應(yīng)用讀取數(shù)據(jù)時锨能,先在內(nèi)核態(tài)將數(shù)據(jù)從磁盤讀取到內(nèi)核緩存算途,再切換到用戶態(tài)將數(shù)據(jù)從內(nèi)核緩存讀取到用戶緩存爪模。寫入操作也是類似,僅僅是步驟相反滋戳。所以斋竞,這種方式會帶來一定的額外開銷倔约,可能會降低IO效率。
? ? ? ?而基于NIOtransferTo的實現(xiàn)方式坝初,在Linux和Unix上浸剩,則會使用到零拷貝技術(shù),數(shù)據(jù)傳輸并不需要用戶態(tài)參與鳄袍,省去了上下文切換的開銷和不必要的內(nèi)存拷貝绢要,進(jìn)而可能提高應(yīng)用拷貝性能。注意拗小,transferTo不僅僅是可以用在文件拷貝中重罪,與其類似的,例如讀取磁盤文件哀九,然后進(jìn)行Socket發(fā)送剿配,同樣可以享受這種機制帶來的性能和擴展性提高。JDK的源代碼中勾栗,內(nèi)部實現(xiàn)和公共API定義也不是可以能夠簡單關(guān)聯(lián)上的惨篱,NIO部分代碼甚至是定義為模板而不是Java源文件,在build過程自動生成源碼围俘,簡單紹一下部分JDK代碼機制和如何繞過隱藏障礙砸讳。
? ? ? ?? 首先,直接跟蹤界牡,發(fā)現(xiàn)FileSystemProvider只是個抽象類簿寂,閱讀它的源碼能夠理解到,原來文件系統(tǒng)實際邏輯存在于JDK內(nèi)部實現(xiàn)里宿亡,公共API其實是通過ServiceLoader機制加載一系列文件系統(tǒng)實現(xiàn)常遂,然后提供服務(wù)。
? ? ? ?? 我們可以在JDK源碼里搜索FileSystemProvider和nio挽荠,可以定位到sun/nio/fs克胳,我們知道NIO底層是和操作系統(tǒng)緊密相關(guān)的平绩,所以每個平臺都有自己的部分特有文件系統(tǒng)邏輯。
? ? ? ?? 省略掉一些細(xì)節(jié)漠另,最后我們一步步定位到UnixFileSystemProvider → UnixCopyFile.Transfer捏雌,發(fā)現(xiàn)這是個本地方法。
? ? ? ?? 最后笆搓,明確定位到UnixCopyFile.c性湿,其內(nèi)部實現(xiàn)清楚說明竟然只是簡單的用戶態(tài)空間拷貝!
? ? ? ?所以满败,我們明確這個最常見的copy方法其實不是利用transferTo肤频,而是本地技術(shù)實現(xiàn)的用戶態(tài)拷貝。如何提高類似拷貝等IO操作的性能算墨,有一些寬泛的原則:在程序中宵荒,使用緩存等機制,合理減少IO次數(shù)(在網(wǎng)絡(luò)通信中米同,如TCP傳輸骇扇,window大小也可以看作是類似思路);使用transferTo等機制面粮,減少上下文切換和額外IO操作;盡量減少不必要的轉(zhuǎn)換過程继低,比如編解碼熬苍;對象序列化和反序列化,比如操作文本文件或者網(wǎng)絡(luò)通信袁翁,如果不是過程中需要使用文本信息柴底,可以考慮不要將二進(jìn)制信息轉(zhuǎn)換成字符串,直接傳輸二進(jìn)制信息粱胜。
4.掌握NIO Buffer
? ? ? ?Java為每種原始數(shù)據(jù)類型都提供了相應(yīng)的Buffer實現(xiàn)(布爾除外)柄驻,所以掌握和使用Buffer是十分必要的,尤其是涉及Direct Buffer等使用焙压,因為其在垃圾收集等方面的特殊性鸿脓,更要重點掌握。
Buffer有幾個基本屬性:
? ? ? ?? capcity涯曲,它反映這個Buffer到底有多大野哭,也就是數(shù)組的長度。
? ? ? ?? position幻件,要操作的數(shù)據(jù)起始位置拨黔。
? ? ? ?? limit,相當(dāng)于操作的限額绰沥。在讀取或者寫入時篱蝇,limit的意義很明顯是不一樣的贺待。比如,讀取操作時零截,很可能將limit設(shè)置到所容納數(shù)據(jù)的上限狠持;而在寫入時,則會設(shè)置容量或容量以下的可寫限度瞻润。
? ? ? ?? mark喘垂,記錄上一次postion的位置,默認(rèn)是0绍撞,算是一個便利性的考慮正勒,往往不是必須的。
? ? ? ?簡單梳理下Buffer的基本操作:我們創(chuàng)建了一個ByteBuffer傻铣,準(zhǔn)備放入數(shù)據(jù)章贞,capcity當(dāng)然就是緩沖區(qū)大小,而position就是0非洲,limit默認(rèn)就是capcity的大醒枷蕖;當(dāng)我們寫入幾個字節(jié)的數(shù)據(jù)時两踏,position就會跟著水漲船高败京,但是它不可能超過limit的大小梦染;如果我們想把前面寫入的數(shù)據(jù)讀出來赡麦,需要調(diào)用flip方法,將position設(shè)置為0帕识,limit設(shè)置為以前的position那里泛粹;如果還想從頭再讀一遍,可以調(diào)用rewind肮疗,讓limit不變晶姊,position再次設(shè)置為0。
4.Direct Buffer和垃圾收集
? ? ? ?? Direct Buffer:如果我們看Buffer的方法定義伪货,你會發(fā)現(xiàn)它定義了isDirect()方法们衙,返回當(dāng)前Buffer是否是Direct類型。這是因為Java提供了堆內(nèi)和堆外(Direct)Buffer超歌,我們可以以它的allocate或者allocateDirect方法直接創(chuàng)建砍艾。
? ? ? ?? MappedByteBuffer:它將文件按照指定大小直接映射為內(nèi)存區(qū)域,當(dāng)程序訪問這個內(nèi)存區(qū)域時將直接操作這塊兒文件數(shù)據(jù)巍举,省去了將數(shù)據(jù)從內(nèi)核空間向用戶空間傳輸?shù)膿p耗脆荷。我們可以使用FileChannel.map創(chuàng)建MappedByteBuffer,它本質(zhì)上也是種Direct Buffer。在實際使用中蜓谋,Java會盡量對Direct Buffer僅做本地IO操作梦皮,對于很多大數(shù)據(jù)量的IO密集操作,可能會帶來非常大的性能優(yōu)勢桃焕,因為:
? ? ? ?? Direct Buffer生命周期內(nèi)內(nèi)存地址都不會再發(fā)生更改剑肯,進(jìn)而內(nèi)核可以安全地對其進(jìn)行訪問,很多IO操作會很高效观堂。
? ? ? ?? 減少了堆內(nèi)對象存儲的可能額外維護(hù)工作让网,所以訪問效率可能有所提高。
? ? ? ? 但是請注意师痕,Direct Buffer創(chuàng)建和銷毀過程中溃睹,都會比一般的堆內(nèi)Buffer增加部分開銷,所以通常都建議用于長期使用胰坟、數(shù)據(jù)較大的場景因篇。使用Direct Buffer,我們需要清楚它對內(nèi)存和JVM參數(shù)的影響笔横。首先竞滓,因為它不在堆上,所以Xmx之類參數(shù)吹缔,其實并不能影響DirectBuffer等堆外成員所使用的內(nèi)存額度商佑,我們可以使用下面參數(shù)設(shè)置大小:
-XX:MaxDirectMemorySize=512M
? ? ? ? 從參數(shù)設(shè)置和內(nèi)存問題排查角度來看涛菠,這意味著我們在計算Java可以使用的內(nèi)存大小的時候莉御,不能只考慮堆的需要,還有Direct Buffer等一系列堆外因素俗冻。如果出現(xiàn)內(nèi)存不足,堆外內(nèi)存占用也是一種可能性牍颈。另外迄薄,大多數(shù)垃圾收集過程中,都不會主動收集Direct Buffer煮岁,它的垃圾收集過程讥蔽,就是基于我在專欄前面所介紹的Cleaner(一個內(nèi)部實現(xiàn))和幻象引用(PhantomReference)機制,其本身不是public類型画机,內(nèi)部實現(xiàn)了一個Deallocator負(fù)責(zé)銷毀的邏輯冶伞。對它的銷毀往往要拖到full GC的時候,所以使用不當(dāng)很容易導(dǎo)致OutOfMemoryError步氏。對于Direct Buffer的回收:
? ? ? ?? 在應(yīng)用程序中响禽,顯式地調(diào)用System.gc()來強制觸發(fā)。
? ? ? ?? 另外一種思路是,在大量使用Direct Buffer的部分框架中芋类,框架會自己在程序中調(diào)用釋放方法隆嗅,Netty就是這么做的,有興趣可以參考其實現(xiàn)(PlatformDependent0)侯繁。
? ? ? ?? 重復(fù)使用Direct Buffer胖喳。
5.跟蹤和診斷Direct Buffer內(nèi)存占用?
? ? ? ?因為通常的垃圾收集日志等記錄贮竟,并不包含Direct Buffer等信息丽焊,所以Direct Buffer內(nèi)存診斷也是個比較頭疼的事情。幸好咕别,在JDK8之后的版本技健,我們可以方便地使用Native Memory Tracking(NMT)特性來進(jìn)行診斷,你可以在程序啟動時加上下面參數(shù):
-XX:NativeMemoryTracking={summary|detail}
? ? ? ?注意顷级,激活NMT通常都會導(dǎo)致JVM出現(xiàn)5%~10%的性能下降凫乖,請謹(jǐn)慎考慮。運行時弓颈,可以采用下面命令進(jìn)行交互式對比:
// 打印NMT信息
jcmd <pid> VM.native_memory detail
// 進(jìn)行baseline帽芽,以對比分配內(nèi)存變化
jcmd <pid> VM.native_memory baseline
// 進(jìn)行baseline,以對比分配內(nèi)存變化
jcmd <pid> VM.native_memory detail.diff
? ? ? ?可以在Internal部分發(fā)現(xiàn)Direct Buffer內(nèi)存使用的信息翔冀,這是因為其底層實際是利用unsafe_allocatememory导街。嚴(yán)格說,這不是JVM內(nèi)部使用的內(nèi)存纤子,所以在JDK11以后搬瑰,其實它是歸類在other部分里。JDK 9的輸出片段如下控硼,“+”表示的就是diff命令發(fā)現(xiàn)的分配變化:
-Internal (reserved=679KB +4KB, committed=679KB +4KB)
(malloc=615KB +4KB #1571 +4)
(mmap: reserved=64KB, committed=64KB)