[轉載] Java直接記憶體與堆記憶體
本文轉載自 http://blogxin.cn/2017/01/31/mappedbytebuffer-zerocopy/
JAVA處理大檔案,一般用BufferedReader,BufferedInputStream這類帶緩衝的IO類,不過如果檔案超大的話,更快的方式是採用MappedByteBuffer。
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, len);
FileChannel提供了map方法來把檔案對映為記憶體映像檔案:可以把檔案的從position開始的size大小的區域對映為記憶體映像檔案,MapMode表示了可訪問該記憶體映像檔案的方式:
READ_ONLY,(只讀): 試圖修改得到的緩衝區將導致丟擲ReadOnlyBufferException。(MapMode.READ_ONLY)
READ_WRITE(讀/寫): 對得到的緩衝區的更改最終將傳播到檔案;該更改對對映到同一檔案的其他程式不一定是可見的。(MapMode.READ_WRITE)
PRIVATE(專用): 對得到的緩衝區的更改不會傳播到檔案,並且該更改對對映到同一檔案的其他程式也不是可見的;相反,會建立緩衝區已修改部分的專用副本。(MapMode.PRIVATE)
MappedByteBuffer是ByteBuffer的子類,其擴充了三個方法:
force():緩衝區是READ_WRITE模式下,此方法對緩衝區內容的修改強行寫入檔案;
load():將緩衝區的內容載入記憶體,並返回該緩衝區的引用;
isLoaded():如果緩衝區的內容在實體記憶體中,則返回真,否則返回假;
對比傳統檔案IO與記憶體對映
在傳統的檔案IO操作中,我們都是呼叫作業系統提供的底層標準IO系統呼叫函式read()、write() ,此時呼叫此函式的程式(在JAVA中即java程式)由當前的使用者態切換到核心態,然後OS的核心程式碼負責將相應的檔案資料讀取到核心的IO緩衝區,然後再把資料從核心IO緩衝區拷貝到程式的私有地址空間中去,這樣便完成了一次IO操作。這麼做是為了減少磁碟的IO操作,為了提高效能而考慮的,因為我們的程式訪問一般都帶有區域性性,也就是所謂的區域性性原理,在這裡主要是指的空間區域性性,即我們訪問了檔案的某一段資料,那麼接下去很可能還會訪問接下去的一段資料,由於磁碟IO操作的速度比直接訪問記憶體慢了好幾個數量級,所以OS根據區域性性原理會在一次read()系統呼叫過程中預讀更多的檔案資料快取在核心IO緩衝區中,當繼續訪問的檔案資料在緩衝區中時便直接拷貝資料到程式私有空間,避免了再次的低效率磁碟IO操作。
記憶體對映檔案是將硬碟上檔案的位置與程式邏輯地址空間中一塊大小相同的區域之間一一對應, 建立記憶體對映由mmap()系統呼叫將檔案直接對映到使用者空間,mmap()中沒有進行資料拷貝,真正的資料拷貝是在缺頁中斷處理時進行的,mmap()會返回一個指標ptr,它指向程式邏輯地址空間中的一個地址,要操作其中的資料時即第一次訪問ptr指向的記憶體區域,必須通過MMU將邏輯地址轉換成實體地址,MMU在地址對映表中是無法找到與ptr相對應的實體地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函式會通過mmap()建立的對映關係,從硬碟上將檔案讀取到實體記憶體中,這個過程只進行了一次資料拷貝。因此,記憶體對映的效率要比read/write呼叫效率高。
通過下面的例子測試普通檔案通道IO和記憶體對映IO的速度:
public class FileChannelTest {
public static void main(String[] args) throws IOException {
testFileChannel();
testMappedByteBuffer();
}
public static void testFileChannel() throws IOException {
RandomAccessFile file = null;
try {
file = new RandomAccessFile("/Users/xin/Downloads/b.txt", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buff = ByteBuffer.allocate(1024);
long timeBegin = System.currentTimeMillis();
while (channel.read(buff) != -1) {
buff.flip();
buff.clear();
}
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void testMappedByteBuffer() throws IOException {
RandomAccessFile file = null;
try {
file = new RandomAccessFile("/Users/xin/Downloads/b.txt", "rw");
FileChannel fc = file.getChannel();
int len = (int) file.length();
MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, len);
byte[] b = new byte[1024];
long timeBegin = System.currentTimeMillis();
for (int offset = 0; offset < len; offset += 1024) {
if (len - offset > 1024) {
buffer.get(b);
} else {
buffer.get(new byte[len - offset]);
}
}
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
控制檯輸出結果如下:
Read time: 302ms
Read time: 61ms
根據測試結果證明了記憶體對映檔案比檔案通道速度快很多。
記憶體對映檔案和之前說的標準IO操作最大的不同之處就在於它雖然最終也是要從磁碟讀取資料,但是它並不需要將資料讀取到OS核心緩衝區,而是直接將程式的使用者私有地址空間中的一部分割槽域與檔案物件建立起對映關係,就好像直接從記憶體中讀、寫檔案一樣,速度當然快了。記憶體對映檔案的效率比標準IO高的重要原因就是因為少了把資料拷貝到OS核心緩衝區這一步。
zerocopy技術
zerocopy技術的目標就是提高IO密集型JAVA應用程式的效能。IO操作需要資料頻繁地在核心緩衝區和使用者緩衝區之間拷貝,而zerocopy技術可以減少這種拷貝的次數,同時也降低了上下文切換(使用者態與核心態之間的切換)的次數。在Java中的應用就是java.nio.channels.FileChannel類的transferTo()方法可以直接將位元組傳送到可寫的通道中,並不需要將位元組轉入使用者緩衝區。
package java.nio.channels;
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
... ...
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
... ...
}
FileChannel fileChannel = fis.getChannel();
fileChannel.transferTo(0, fileChannel.size(), targetChannel);
正常讀取檔案再傳送出去需要經歷一下幾個步驟:
從本地磁碟或者網路讀取資料--->資料進入核心緩衝區--->使用者緩衝區--->核心緩衝區--->通過socket傳送
資料每次在核心緩衝區與使用者緩衝區之間的拷貝會消耗CPU以及記憶體的頻寬。而zerocopy有效減少了這種拷貝次數,使用者程式執行transferTo()方法,導致一次系統呼叫,從使用者態切換到核心態,完成的動作是:
從本地磁碟或者網路讀取資料--->資料進入核心緩衝區--->通過socket傳送
zerocopy好處就是減少了將資料從核心緩衝區拷貝到使用者緩衝區,再拷貝回核心緩衝區,減少了拷貝次數和上下文切換次數。
相關文章
- 直接記憶體和堆記憶體誰快記憶體
- Java堆外直接記憶體回收Java記憶體
- 堆記憶體和棧記憶體詳解(轉載)記憶體
- Java堆記憶體Heap與非堆記憶體Non-HeapJava記憶體
- Java直接(堆外)記憶體使用詳解Java記憶體
- java棧記憶體和堆記憶體的詮釋Java記憶體
- 直接記憶體記憶體
- Java 堆疊記憶體分配Java記憶體
- java 堆外記憶體排查Java記憶體
- JS中的棧記憶體、堆記憶體JS記憶體
- 記憶體堆疊記憶體
- jvm 堆記憶體JVM記憶體
- 深入理解Java的堆記憶體和執行緒記憶體Java記憶體執行緒
- NameNode堆記憶體估算記憶體
- JAVA堆外記憶體排查小結Java記憶體
- JAVA記憶體管理 [轉]Java記憶體
- Java的記憶體 -JVM 記憶體管理Java記憶體JVM
- JAVA記憶體區域與記憶體溢位異常Java記憶體溢位
- Go:記憶體管理與記憶體清理Go記憶體
- 聊聊 記憶體模型與記憶體序記憶體模型
- JVM堆記憶體詳解JVM記憶體
- JVM堆記憶體設定JVM記憶體
- Tomcat增加堆記憶體Tomcat記憶體
- 【JVM之記憶體與垃圾回收篇】堆JVM記憶體
- Java記憶體區域和記憶體模型Java記憶體模型
- 【轉】java中的記憶體溢位和記憶體洩漏Java記憶體溢位
- 記憶體分析與記憶體洩漏定位記憶體
- 【Java基礎】實體記憶體&虛擬記憶體Java記憶體
- 程式的記憶體分配:棧區(stack)堆區(heap)。。。(轉載)記憶體
- 哪個更快:Java堆還是本地記憶體Java記憶體
- 轉載——C++記憶體管理C++記憶體
- JVM(2)-Java記憶體區域與記憶體溢位異常JVMJava記憶體溢位
- JVM 堆記憶體設定原理JVM記憶體
- 遺失的JVM堆記憶體JVM記憶體
- 託管堆記憶體佔用記憶體
- JVM——記憶體洩漏與記憶體溢位JVM記憶體溢位
- Redis記憶體——記憶體消耗(記憶體都去哪了?)Redis記憶體
- 從記憶體洩露、記憶體溢位和堆外記憶體,JVM優化引數配置引數記憶體洩露記憶體溢位JVM優化