程序運行在內存以及IO的體現
首先普及一下常識,如圖所示:
1、在整個內存空間中,跑着各種各樣的程序,有Java程序、C程序,他們共用一塊內存空間。
2、對於Java程序,JVM會申請一塊堆空間,通過Xmx可以設置,其余空間是堆外空間,其中每個線程有自己的線程棧,保證線程內存隔離,堆空間使用完以后,會觸發Full FC,堆外空間所有進程可共享使用,無限制。
3、所有系統運行的程序都必須通過操作系統內核進行IO操作,操作系統也是程序,也需要一定的內存空間。
一、使用Buffer代替基本IO
我們寫一個方法,此方法使用了FileWriter進行了文件的寫操作,我們都知道不調用flush()可能會造成數據丟失,那么為什么呢,flush操作到底做了些什么呢?
public void fileIO() throws Exception { File file = new File("/Volumes/work/temp/temp.txt"); if (file.exists()) { file.delete(); } file.createNewFile(); FileInputStream fileInputStream = new FileInputStream(file); FileWriter fileWriter = new FileWriter(file); fileWriter.write("hello"); fileWriter.write("world"); fileWriter.write("\nhello world"); Thread.sleep(99999); fileWriter.flush(); fileWriter.close(); }
我們知道我們在寫數據的時候不管是C還是Java都會有兩個緩沖區,一個是操作系統的緩沖區sys buffer,還有一個是程序的緩沖區program buffer。那么剛剛的flush操作是把程序的緩沖區內容寫到了系統緩沖區,還是把系統緩沖區的內容刷到了硬盤呢?因此我們在調用flush()之前進行了sleep操作,檢查在flush之前,具體的內容並未寫到temp.txt文件中,當我們睡眠時間結束后,可以看到調用flush方法后則把內容寫到了文件中,如圖:
實際上FileWriter基本IO是沒有先寫程序緩存的,那么實際上FileWriter的每次write操作都發生了系統調用,直接寫到了內核的系統緩沖區,然后當調用flush操作時,系統緩沖區的內容再刷到了硬盤上。
因此IO性能提升第一步:無論是InputStream還是FileWriter,都是底層的IO,是直接調用內核的,因此寫入都是直接寫入到內核的系統buffer,因此在使用IO的時候不要使用這類底層IO,否則發生大量系統調用,降低系統性能,而是應該先寫到程序buffer然后再調用系統IO,當程序buffer滿了后才通過系統調用寫到系統buffer空間中,這樣減少了大量系統調用,提升了性能。
那么什么時候系統buffer中的數據才寫入到硬盤呢?2種情況:①.系統buffer滿了;②.執行了flush()操作,也就是發生了fsync的系統調用。
public void bufferedIO() throws Exception { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file), 1024); BufferedReader reader = new BufferedReader(new FileReader(file)); bufferedOutputStream.write("hello world\nhello world".getBytes()); bufferedOutputStream.flush(); bufferedOutputStream.close(); String line = reader.readLine(); System.out.println(line); }
還有另一種是直接寫入到內存的,如代碼:
public void memoryIO() throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1024); // 字節數組輸出流在內存中創建一個字節數組緩沖區,所有發送到輸出流的數據保存在該字節數組緩沖區中。可以通過toString()和toByteArray()獲取數據 byteArrayOutputStream.write("hello world".getBytes()); String string = byteArrayOutputStream.toString(); System.out.println(string); byte[] inData = byteArrayOutputStream.toByteArray(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inData); byte[] data = new byte[1024]; byteArrayInputStream.read(data); System.out.println(new String(data)); byteArrayOutputStream.flush(); byteArrayOutputStream.close(); }
這樣就類似於Redis一樣,是對內存進行直接操作,因此這樣也能提高不少效率。
二、堆外內存mmap直接映射內核空間
如下圖:
1、如果數據在堆內,那么在寫入磁盤時,會先序列化后拷貝到堆外,然后堆外再write到系統內核緩沖區,內核緩沖區通過系統調用fsync寫入到磁盤;
2、如果數據是在堆外內存,那么也需要先拷貝到內核緩沖區,在fsync系統調用后也才寫入到磁盤;
3、通過系統調用mmap申請一塊虛擬的地址空間,這片空間用戶程序和系統內核都可以訪問到。
如下代碼:
public void randomIO() throws Exception{ RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw"); randomAccessFile.write("hello world\nhello chicago\nhello ChengDu".getBytes()); FileChannel channel = randomAccessFile.getChannel(); /** * 堆外的數據如果想寫磁盤,通過系統調用,經歷數據從用戶空間拷貝到內核空間 * 堆外mapedBuffer的數據內核直接處理 */ // 分配在了堆上 heap空間 // ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 分配在了堆外 offheap空間 // ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); //mmap 內核系統調用 堆外空間,直接映射 MappedByteBuffer byteBuffer = channel.map(FileChannel.MapMode.READ_WRITE,0,2018); byteBuffer.put("byteBuffer testing".getBytes()); randomAccessFile.seek(12); randomAccessFile.write("*****".getBytes()); }
可以看到通過FileChannel的map方法實現系統調用,申請mmap直接映射空間,數據無需由用戶空間拷貝到系統空間,節省了一次拷貝的時間損耗,提升了性能。
三、sendfile零拷貝
在Linux系統中。存儲在文件中的信息通過網絡傳送給客戶這樣的簡單過程中,所涉及的操作。下面是其中的部分簡單代碼:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
其實過程中實現了多次拷貝,性能很低,如圖可知:
步驟一:系統調用read導致了從用戶空間到內核空間的上下文切換。DMA模塊從磁盤中讀取文件內容,並將其存儲在內核空間的緩沖區內,完成了第1次復制。
步驟二:數據從內核空間緩沖區復制到用戶空間緩沖區,完成了第2次復制,之后系統調用read返回,這導致了從內核空間向用戶空間的上下文切換。此時,需要的數據已存放在指定的用戶空間緩沖區內(參數tmp_buf),程序可以繼續下面的操作。
步驟三:系統調用write導致從用戶空間到內核空間的上下文切換。數據從用戶空間緩沖區被再次復制到內核空間緩沖區,完成了第3次復制。不過,這次數據存放在內核空間中與使用的socket相關的特定緩沖區中,而不是步驟一中的緩沖區。
步驟四:系統調用返回,導致了第4次上下文切換。第4次復制在DMA模塊將數據從內核空間緩沖區傳遞至協議引擎的時候發生,這與我們的代碼的執行是獨立且異步發生的。你可能會疑惑:“為何要說是獨立、異步?難道不是在write系統調用返回前數據已經被傳送了?write系統調用的返回,並不意味着傳輸成功——它甚至無法保證傳輸的開始。調用的返回,只是表明以太網驅動程序在其傳輸隊列中有空位,並已經接受我們的數據用於傳輸。可能有眾多的數據排在我們的數據之前。除非驅動程序或硬件采用優先級隊列的方法,各組數據是依照FIFO的次序被傳輸的(圖1中叉狀的DMA copy表明這最后一次復制可以被延后)。
因此就誕生了零拷貝:
sendfile(socket, file, len);
如圖:
步驟一:sendfile系統調用導致文件內容通過DMA模塊被復制到內核緩沖區中。
步驟二:記錄數據位置和長度的描述符被加入到socket緩沖區中,DMA模塊將數據直接從內核緩沖區傳遞給協議引擎。
基於以上實現,最終實現了“零拷貝”。
高性能IO應用
在現實應用中,Kafka常用來進行日志處理,存在着大量的IO,其高性能就是建立在IO上的優化,如圖: