直接在堆外分配一個記憶體(即,native memory)來存儲數據,程式通過JNI直接將數據讀/寫到堆外記憶體中。因為數據直接寫入到了堆外記憶體中,所以這種方式就不會再在JVM管控的堆內再分配記憶體來存儲數據了,也就不存在堆內記憶體和堆外記憶體數據拷貝的操作了。這樣在進行I/O操作時,只需要將這個堆外記憶體地址... ...
java.io 核心概念是流,即面向流的編程,在java中一個流只能是輸入流或者輸出流,不能同時具有兩個概念。
java.nio核心是 selector、Channel、Buffer ,是面向緩衝區(buffer)或者面向塊block。
一、Buffer
Buffer本身是一個記憶體塊,底層是數組,數據的讀寫都是通過Buffer類實現的。即同一個Buffer即可以寫數據也可以讀數據,通過intBuffer.flip()方法進行Buffer位置狀態的翻轉。JAVA中的8中基本類型都有各自對應的Buffer。
緩衝區buffer主要是和通道數據交互,即從通道中讀入數據到緩衝區,和從緩衝區中把數據寫入到通道中,通過這樣完成對數據的傳輸。 它通過幾個變數來保存這個數據的當前位置狀態。
Buffer中的四個核心變數
- 容量(Capacity):緩衝區能夠容納的數據元素的最大數量。這一個容量在緩衝區創建時被設定,並且永遠不能改變。
- 界限(Limit):指定還有多少數據需要取出(在從緩衝區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩衝區時)。
- 位置(Position):指定了下一個將要被寫入或者讀取的元素索引,它的值由get()/put()方法自動更新,在新創建一個Buffer對象時,position被初始化為0。
- 標記(Mark):下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新
get()方法從緩衝區中讀取數據寫入到輸出通道,這會導致position的增加而limit保持不變,但position不會超過limit的值。
flip()方法 把limit設置為當前的position值 並且把position設置為 0
clear()方法將Buffer恢復到初始化狀態
public class BufferTest { public static void main(String[] args) throws IOException { ByteBufferTest(); } private static void ByteBufferTest(){ //分配新的byte緩衝區,參數為緩衝區容量 //新緩衝區的當前位置將為零,其界限(限制位置)將為其容量,它將具有一個底層實現數組,其數組偏移量將為零。 ByteBuffer byteBuffer=ByteBuffer.allocate(10); output("初始化緩衝區:",byteBuffer); for(int i=0;i<byteBuffer.capacity()-1;i++){ byteBuffer.put(Byte.parseByte(new SecureRandom().nextInt(20)+"")); } output("寫入緩衝區9個byte:",byteBuffer); byteBuffer.flip(); output("使用flip重置元素位置:",byteBuffer); while (byteBuffer.hasRemaining()){ System.out.print(byteBuffer.get()+"|"); } System.out.print("\n"); output("使用get讀取元素:",byteBuffer); byteBuffer.clear(); output("恢復初始化態clear:",byteBuffer); } private static void output(String step, Buffer buffer) { System.out.println(step + " : "); System.out.print("capacity: " + buffer.capacity() + ", "); System.out.print("position: " + buffer.position() + ", "); System.out.println("limit: " + buffer.limit()); System.out.println("mark: " + buffer.mark()); System.out.println(); } } 初始化緩衝區: : capacity: 10, position: 0, limit: 10 mark: java.nio.HeapByteBuffer[pos=0 lim=10 cap=10] 寫入緩衝區9個byte: : capacity: 10, position: 9, limit: 10 mark: java.nio.HeapByteBuffer[pos=9 lim=10 cap=10] 使用flip重置元素位置: : capacity: 10, position: 0, limit: 9 mark: java.nio.HeapByteBuffer[pos=0 lim=9 cap=10] 讀取元素:1|讀取元素:16|讀取元素:12|讀取元素:0|讀取元素:17|讀取元素:5|讀取元素:4|讀取元素:13|讀取元素:18| 使用get讀取元素後: : capacity: 10, position: 9, limit: 9 mark: java.nio.HeapByteBuffer[pos=9 lim=9 cap=10] 恢復初始化態clear: : capacity: 10, position: 0, limit: 10 mark: java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
ByteBuffer.wrap( array ):將一個現有的數組,包裝為緩衝區對象
buffer.slice():創建子緩衝區,子緩衝區與原緩衝區是數據共用的
buffer.position( 3 ); buffer.limit( 7 ); ByteBuffer slice = buffer.slice();
只讀緩衝區:ByteBuffer readonly = buffer.asReadOnlyBuffer();
只讀緩衝區非常簡單,可以讀取它們,但是不能向它們寫入數據。可以通過調用緩衝區的asReadOnlyBuffer()方法,將任何常規緩衝區轉 換為只讀緩衝區,這個方法返回一個與原緩衝區完全相同的緩衝區,並與原緩衝區共用數據,只不過它是只讀的。如果原緩衝區的內容發生了變化,只讀緩衝區的內容也隨之發生變化。
如果嘗試修改只讀緩衝區的內容,則會報ReadOnlyBufferException異常。只讀緩衝區對於保護數據很有用。創建一個只讀的緩衝區可以保證該緩衝區不會被修改。只可以把常規緩衝區轉換為只讀緩衝區,而不能將只讀的緩衝區轉換為可寫的緩衝區。
二、直接緩衝區 DirectByteBuffer
直接在堆外分配一個記憶體(即,native memory)來存儲數據,程式通過JNI直接將數據讀/寫到堆外記憶體中。因為數據直接寫入到了堆外記憶體中,所以這種方式就不會再在JVM管控的堆內再分配記憶體來存儲數據了,也就不存在堆內記憶體和堆外記憶體數據拷貝的操作了。這樣在進行I/O操作時,只需要將這個堆外記憶體地址傳給JNI的I/O的函數就好了。底層的數據其實是維護在操作系統的記憶體中,而不是jvm里,DirectByteBuffer里維護了一個引用address指向了數據,從而操作數據。實現zero copy(零拷貝)。
間接記憶體HeapByteBuffer:對於HeapByteBuffer,數據的分配存儲都在jvm堆上,當需要和io設備打交道的時候,會將jvm堆上所維護的byte[]拷貝至堆外記憶體,然後堆外記憶體直接和io設備交互。外設之所以要把jvm堆里的數據copy出來再操作,不是因為操作系統不能直接操作jvm記憶體,而是因為jvm在進行gc(垃圾回收)時,會對數據進行移動,一旦出現這種問題,外設就會出現數據錯亂的情況。
直接緩衝區的創建:ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );
DirectByteBuffer的初始化:
DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); // 保留總分配記憶體(按頁分配)的大小和實際記憶體的大小 Bits.reserveMemory(size, cap); long base = 0; try { // 通過unsafe.allocateMemory分配堆外記憶體,並返回堆外記憶體的基地址 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } // 構建Cleaner對象用於跟蹤DirectByteBuffer對象的垃圾回收,以實現當DirectByteBuffer被垃圾回收時,堆外記憶體也會被釋放 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
// Used only by direct buffers // NOTE: hoisted here for speed in JNI GetDirectBufferAddress //address就是堆外記憶體創建好後返回給JVM的地址,JVM記憶體需要維護的只是DirectByteBuffer對象,而具體數據的管理是由操作系統來管理的 long address;
什麼情況下使用堆外記憶體
- 堆外記憶體適用於生命周期中等或較長的對象。( 如果是生命周期較短的對象,在YGC的時候就被回收了,就不存在大記憶體且生命周期較長的對象在FGC對應用造成的性能影響 )。
- 直接的文件拷貝操作,或者I/O操作。直接使用堆外記憶體就能少去記憶體從用戶記憶體拷貝到系統記憶體的操作,因為I/O操作是系統內核記憶體和設備間的通信,而不是通過程式直接和外設通信的。
- 同時,還可以使用 池+堆外記憶體 的組合方式,來對生命周期較短,但涉及到I/O操作的對象進行堆外記憶體的再使用。( Netty中就使用了該方式 )
兩種方式的效率比較:
private static void directByteBufferTest()throws IOException{ long start=System.currentTimeMillis(); FileInputStream is=new FileInputStream("F:\\logs\\1g.rar"); FileOutputStream fos=new FileOutputStream("F:\\logs\\2g.rar"); FileChannel fcIs,fcOut; fcIs=is.getChannel(); fcOut=fos.getChannel(); ByteBuffer directByteBuffer= ByteBuffer.allocateDirect(2048); while (fcIs.read(directByteBuffer)!=-1){ directByteBuffer.flip(); fcOut.write(directByteBuffer); directByteBuffer.clear(); } is.close(); fos.close(); long end=System.currentTimeMillis(); System.out.println("DirectByteBuffer需要時間:"+(end-start)); } private static void heapByteBufferTest()throws IOException{ long start=System.currentTimeMillis(); FileInputStream is=new FileInputStream("F:\\logs\\1g.rar"); FileOutputStream fos=new FileOutputStream("F:\\logs\\3g.rar"); FileChannel fcIs,fcOut; fcIs=is.getChannel(); fcOut=fos.getChannel(); ByteBuffer directByteBuffer= ByteBuffer.allocate(2048); while (fcIs.read(directByteBuffer)!=-1){ directByteBuffer.flip(); fcOut.write(directByteBuffer); directByteBuffer.clear(); } is.close(); fos.close(); long end=System.currentTimeMillis(); System.out.println("HeapByteBuffer需要時間:"+(end-start)); }
17行輸出:DirectByteBuffer需要時間:30456
35行輸出:HeapByteBuffer需要時間:45285
三、記憶體映射文件I/O MappedByteBuffer
記憶體映射文件I/O是一種讀和寫文件數據的方法,它可以比常規的基於流或者基於通道的I/O快的多。記憶體映射文件I/O是通過使文件中的數據出現為 記憶體數組的內容來完成的,這其初聽起來似乎不過就是將整個文件讀到記憶體中,但是事實上並不是這樣。一般來說,只有文件中實際讀取或者寫入的部分才會映射到記憶體中。
FileChannel提供了map方法來把文件影射為記憶體映像文件: MappedByteBuffer map(int mode,long position,long size); 可以把文件的從position開始的size大小的區域映射為記憶體映像文件,映射記憶體緩衝區是個直接緩衝區,繼承自ByteBuffer,但相對於ByteBuffer,它有更多的優點 讀取快 寫入快 隨時隨地寫入;
mode指出了 可訪問該記憶體映像文件的方式:
1、READ_ONLY,(只讀): 試圖修改得到的緩衝區將導致拋出 ReadOnlyBufferException.(MapMode.READ_ONLY)
2、READ_WRITE(讀/寫): 對得到的緩衝區的更改最終將傳播到文件;該更改對映射到同一文件的其他程式不一定是可見的。 (MapMode.READ_WRITE)
3、PRIVATE(專用): 對得到的緩衝區的更改不會傳播到文件,並且該更改對映射到同一文件的其他程式也不是可見的;相反,會創建緩衝區已修改部分的專用副本。 (MapMode.PRIVATE)
MappedByteBuffer 中的三個方法:
a. fore();緩衝區是READ_WRITE模式下,此方法對緩衝區內容的修改強行寫入文件
b. load()將緩衝區的內容載入記憶體,並返回該緩衝區的引用
c. isLoaded()如果緩衝區的內容在物理記憶體中,則返回真,否則返回假
使用MappedByteBuffer 將數據寫入文件:
private static void mappedOutFile()throws IOException{ String str="I Love MappedByteBuffer"; RandomAccessFile raf = new RandomAccessFile( filePath, "rw" ); FileChannel fc = raf.getChannel(); byte [] msg=str.getBytes("UTF-8"); MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, msg.length); mbb.put(msg); fc.write(mbb); raf.close(); }