Netty源码分析章节概览
前面我们做了很多基础知识的准备,包括bio,nio,aio的理论和概念。BIO实现原理和通信原理知识的梳理等。下面我们深入的分析下NIO相关的知识。和Socket类和ServerSocket类相对应的,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字接口实现。当然这两种新增的模式支持阻塞模式和非阻塞模式。
NIO在JDK1.4中引入的。主要用来弥补原来同步阻塞I/O的不足。以后的几个章节中详细分析:Buffer、Channel、Selector
Buffer从名字可以很直观的看出,它是一个缓冲区。它可以缓冲一些需要写入和读出的数据。传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。
入门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);
}
}
}
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;
}
}
......
}
Buffer一些常用的类实现。这里我们主要分析的是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);
}
......
}
这里构造函数采用了缺省方式。其次,创建对象采用的方式是静态工厂的方式创建对象。
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内存进行管理的。
堆外内存是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;
}
把当前的position赋值给mark
public final Buffer mark() {
mark = position;
return this;
}
把mark值还原给position
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
一旦读完Buffer中的数据,需要让Buffer准备好再次被写入,clear会恢复状态值,但不会擦除数据
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
Buffer有两种模式,写模式和读模式,flip后Buffer从写模式变成读模式
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
堆外内存清理,有两种方式,一种是JVM进行full gc的时候,会清理堆外内存,这种情况是不可控制的。通常情况下,我们可以自己手动清理堆外内存。以下是部分代码块
public static void clean(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
// 手动释放清理堆外内存空间
((DirectBuffer)byteBuffer).cleaner().clean();
}
}
至此Buffer源码已经分析完成,通过分析可以知道,Buffer内部维护着一个字节数组。字节组数的实现方式分为两种,一个是jvm内存中申请,另外一种通过堆外内存申请。同时数据内部维护着几个位置变量。分别为:limit,position,capacity,mark。在读模式和写模式下不断的切换,达到缓存数据的目的。