章節概覽
1、概述
前面我們做了很多基礎知識的準備,包括bio,nio,aio的理論和概念。BIO實現原理和通信原理知識的梳理等。下面我們深入的分析下NIO相關的知識。和Socket類和ServerSocket類相對應的,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字接口實現。當然這兩種新增的模式支持阻塞模式和非阻塞模式。
2、NIO類庫簡介
NIO在JDK1.4中引入的。主要用來彌補原來同步阻塞I/O的不足。以後的幾個章節中詳細分析:Buffer、Channel、Selector
2.1 Buffer深入分析
Buffer從名字可以很直觀的看出,它是一個緩衝區。它可以緩衝一些需要寫入和讀出的數據。傳統的IO操作面向數據流,意味着每次從流中讀一個或多個字節,直至完成,數據沒有被緩存在任何地方。NIO操作面向緩衝區,數據從Channel讀取到Buffer緩衝區,隨後在Buffer中處理數據。
2.1.1 Buffer入門Demo
入門demo很簡單,就是從文件管道中讀取數據,輸出到控制檯。
public class BufferDemo {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("D:\\gc.log");
FileChannel channel = fileInputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(20);
int read = channel.read(buffer);
while (read != -1) {
// 在讀取數據之前,需要重置下limit參數,讓limit = position;
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 讀取結束之後,需要重置下參數limit = capacity;
buffer.clear();
read = channel.read(buffer);
}
}
}
2.1.2、Buffer 構造器和成員變量
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
// mark 用來臨時存儲當前position的值
private int mark = -1;
// 當前數據遊標的位置,在讀寫模式下是不同的:
// 在寫模式下,每次添加一個字節,position++。
// 在讀模式下,先把 limit = position,在把position = 0; 這樣每次讀取一個字節position++,直到 position < limit 結束。
private int position = 0;
// limit 在讀寫模式下是不同的:
// 寫模式下,limit表示最多能往Buffer裏寫多少數據,等於capacity值;
// 讀模式下,limit表示最多可以讀取多少數據,limit = position。
private int limit;
// 表示當前緩衝區的容量大小
private int capacity;
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
// Creates a new buffer with the given mark, position, limit, and capacity,
// after checking invariants.
//
// Buffer 構造函數
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
......
}
2.1.3、Buffer 類繼承關係說明
Buffer一些常用的類實現。這裏我們主要分析的是ByteBuffer,其他的類關係以此類推。
2.1.3、ByteBuffer 構造函數和成員變量
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{
// 在使用堆模式下,創建的hb字節數組,該自己數組由JVM進行管理
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
// 構造函數
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
// 缺省構造函數,主要爲了滿足於創建非堆字節數組而建立
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
// 創建堆外數組,通過DirectByteBuffer 實現類進行實現。
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// 創建由JVM管理的字節數組。
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
......
}
這裏構造函數採用了缺省方式。其次,創建對象採用的方式是靜態工廠的方式創建對象。
2.1.3、HeapByteBuffer 源碼詳解
class HeapByteBuffer
extends ByteBuffer
{
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
HeapByteBuffer(byte[] buf, int off, int len) { // package-private
super(-1, off, off + len, buf.length, buf, 0);
/*
hb = buf;
offset = 0;
*/
}
protected HeapByteBuffer(byte[] buf,
int mark, int pos, int lim, int cap,
int off)
{
super(mark, pos, lim, cap, buf, off);
/*
hb = buf;
offset = off;
*/
}
從HeapByteBuffer源碼中可以發現,通過自己創建一個字節數組,或者外部傳入一個字節數組。來構造ByteBuffer對象。該字節數組完全由jvm內存進行管理的。
2.1.3、DirectBuffer 源碼詳解
堆外內存是java直接申請JVM以外的內存。這樣做的好處是減少jvm gc的時間,因爲對象被創建在堆外。其次申請的堆外直接內存,減少數據從堆外內存拷貝到JVM內存的時間。
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 {
// 通過Java unsafe 分配堆外內存
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;
}
3、ByteBuffer 常用方法
3.1、 mark()
把當前的position賦值給mark
public final Buffer mark() {
mark = position;
return this;
}
3.2、reset()
把mark值還原給position
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
3.3、clear()
一旦讀完Buffer中的數據,需要讓Buffer準備好再次被寫入,clear會恢復狀態值,但不會擦除數據
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
3.4、flip()
Buffer有兩種模式,寫模式和讀模式,flip後Buffer從寫模式變成讀模式
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
3.5、rewind()
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
3.6、sun.misc.Cleaner#clean 堆外內存清理
堆外內存清理,有兩種方式,一種是JVM進行full gc的時候,會清理堆外內存,這種情況是不可控制的。通常情況下,我們可以自己手動清理堆外內存。以下是部分代碼塊
public static void clean(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
// 手動釋放清理堆外內存空間
((DirectBuffer)byteBuffer).cleaner().clean();
}
}
4、 小結
至此Buffer源碼已經分析完成,通過分析可以知道,Buffer內部維護着一個字節數組。字節組數的實現方式分爲兩種,一個是jvm內存中申請,另外一種通過堆外內存申請。同時數據內部維護着幾個位置變量。分別爲:limit,position,capacity,mark。在讀模式和寫模式下不斷的切換,達到緩存數據的目的。