作用
NIO提供了一系列buffer類,用作緩存。可以直接從channel中讀數據到buffer,也可以從buffer中寫數據到channel。緩衝區本質上是一塊固定大小的內存,其作用是一個存儲器或運輸器。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。
類圖
Buffer的四個屬性
- 容量(capacity):緩衝區能夠容納的數據元素的最大數量。這一容量在緩衝區創建時被設定,並且永遠不能被改變
- 上界(limit):緩衝區的第一個不能被讀或寫的元素。或者說,緩衝區中現存元素的計數
- 位置(position):下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新
- 標記(mark):下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )和 put( )函數更新一個備忘位置。調用 mark( )來設定 mark = postion。調用 reset( )設定 position =mark。標記在設定前是未定義的(undefined)。這四個屬性之間總是遵循以下關係:0 <= mark <= position <= limit <= capacity
Buffer的基本用法
使用Buffer讀寫數據一般遵循以下四個步驟:
- 寫入數據到Buffer
- 調用flip()方法
- 從Buffer中讀取數據
- 調用clear()方法或者compact()方法
當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數據。
一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
舉個栗子:
定義一個容量是10的buffer,填入hello:
這時候,limit代表,最多可以寫多少,position代表,即將寫入的位置
然後進行flip:
flip之後,limit代表最多可以讀多少,position,代表開始讀的位置。
三種類型buffer
java nio提供了三種不同的buffer,HeapByteBuffer、DirectByteBuffer、MappedByteBuffer。
一些不同:
- HeapByteBuffer是在jvm堆上申請的內存,而DirectByteBuffer、MappedByteBuffer是在堆外申請的內存。
- MappedByteBuffer藉助了mmap(內存映射文件),提高了文件讀取效率
transferTo:適用於應用程序無需對文件數據進行任何操作的場景;
map:適用於應用程序需要操作文件數據的場景;
HeapByteBuffer
在java堆上申請的內存。通過ByteBuffer.allocate方法申請jvm堆上內存。
看下ByteBuffer.allocate這個方法:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
直接new了一個HeapByteBuffer對象,下面看下這個構造方法:
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
構造方法中調用了父類,ByteBuffer的構造方法
super(-1, 0, lim, cap, new byte[cap], 0);
其中new byte[cap],這裏就能夠確定,HeapByteBuffer申請的內存,確實是在jvm堆上。
DirectByteBuffer
在jvm堆外(OS堆上)申請的內存。通過ByteBuffer的allocateDirect申請。
看下ByteBuffer的allocateDirect方法:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
直接new 了一個DirectByteBuffer對象,看下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 {
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 = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
其中申請內存的操作是通過unsafe類進行的
base = unsafe.allocateMemory(size);
Unsafe的allocateMemory方法通過c的malloc方法【底層依賴兩個系統調用,一個brk,一個mmap】進行進程堆上內存的分配。下面,我們驗證下這個想法:
這裏用到了幾個涉及到jvm監控和linux內存監控的知識:
- Native Memory Tracking(NMT) jdk7提供的內存工具,跟蹤JVM內部的內存使用
- linux的/proc/pid/maps 文件,可以查看進程的虛擬地址空間是如何使用的。
這裏不對這兩塊知識進行再補充,只注重分析內存情況。
測試代碼:
public static void main(String[] args) throws Exception {
ByteBuffer b = ByteBuffer.allocateDirect(1024*1024*50);
//反射獲取Buffer中 的address屬性
Field field = b.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("address");
//打開私有訪問
field.setAccessible(true);
System.out.println(field.get(b).toString());
while (true) {
Thread.sleep(10000);
}
}
代碼中申請了50M的堆外內存。而且會打印出native代碼申請的內存的地址起始地址。
java命令參數如下:注意下-Xms20m -Xmx100m
java -Djava.rmi.server.hostname=49.234.60.90 -Dcom.sun.management.jmxremote.port=2990 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:NativeMemoryTracking=detail -Xms20m -Xmx100m NonBlockServer
這裏打印出了申請的地址的虛擬內存地址起始地址:140260761661472,轉換成16進制是,7f9100dff020
下面看下NMT的情況:
着重分析下jvm堆內存情況:
這裏,說明java堆內存申請了100M,當前使用的爲20M。
下面看具體的堆內存信息:
這裏堆內存的虛擬內存地址爲: 0x00000000f9c00000 - 0x0000000100000000
下面看下,linux的java進程的內存情況(通過查看/proc/pid/maps 文件):
上文中,我們打印的申請的內存的起始地址爲7f9100dff020。
乍一看,maps文件並沒有這個起始地址,但是注意這樣一個內存塊:7f9100dff000-7f9104000000。7f9100dff020剛好在這個區間中。但是7f9100dff000-7f9100dff020這塊多申請的內存,不知道是做什麼用的。
至此,我們驗證了,ByteBuffer的allocateDirect申請的內存並不在jvm堆上,而是在進程堆內存中。
MappedByteBuffer
java對內存文件映射的支持。
內存文件映射的原理:
普通的read io操作原理:
mmap操作的原理:
mmap的核心是,通過使得內核空間和用戶空間的虛擬地址映射到同一塊物理內存上,進而減少了文件在內核空間和用戶空間的拷貝。
下面用一個例子,簡單看下,MappedByteBuffer在內存文件映射操作時的java進程內存和jvm內存情況.
public static void main(String[] args) throws Exception {
String path = ModelDubboService.class.getClassLoader().getResource("test.txt").getPath();
File file = new File(path);
FileChannel fileChannel = new FileInputStream(file).getChannel();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
//反射獲取Buffer中 的address屬性
Field field = mappedByteBuffer.getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredField("address");
//打開私有訪問
field.setAccessible(true);
System.out.println(field.get(mappedByteBuffer).toString());
String path2 = ModelDubboService.class.getClassLoader().getResource("test2.txt").getPath();
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[1024*2014*40];
fileInputStream.read(bytes);
while(true){
Thread.sleep(10000);
}
}
主要是看下,通過普通的讀文件操作和mmap方式讀文件有什麼區別。test.txt使用內存映射的方式來讀,test2.txt使用普通的read方法來讀。
java命令參數如下
java -Djava.rmi.server.hostname=49.234.60.90 -Dcom.sun.management.jmxremote.port=2990 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:NativeMemoryTracking=detail -Xms20m -Xmx100m TestMMap
獲取打印的內存地址:
地址139907108438016,轉換成16進制,7f3ea9800000
下面看下jvm堆內存情況:
jvm最大內存是200M,使用了166M左右。
獲取jvm堆內存的虛擬地址:0x00000000f3800000 - 0x0000000100000000
下面看下linux進程的內存(/proc/pid/maps):
爲了再驗證下,到底是不是通過mmap讀取的文件,我們再看下/proc/pid/map_files這個文件:
這裏就可以確認,test.txt確實是通過mmap讀取的,而text2.txt通過一般的read來讀取的,已經從pageCache拷貝到jvm堆上了。
這裏總結幾個java通過mmap方式相比於普通read方式的有點:
- 不使用jvm堆內存,使用堆外內存,不會對jvm內存造成很大影響
- mmap方式相比read,減少了一次拷貝操作(內核空間->用戶空間),速度更加快
同樣也有一些需要注意的地方:
- mmap使用的堆外內存的回收問題
- mmap適用於不修改文件的場景,如果需要修改文件,則還是要把文件拷貝到jvm堆內存中去操作