Mina中的 IoBuffer 主要是对 Java NIO 中 ByteBuffer 的封装和替换,为什么不直接使用NIO中的ByteBuffer呢?Mina的解释
再想想另外一方面, NIO中的 ByteBuffer 与 Byte 又有什么区别,为什么不直接用 Java IO进行读写操作呢?
网上可查,主要是I/O性能的问题,如果直接使用普通I/O进行读写,那么会占用系统的CPU时间,而如果使用Buffer进行I/O操作,那么CPU将会使用DMA方式执行I/O操作,自己去执行其它的操作,等待DMA读取到Buffer中完成发送一个“完成”消息给CPU,接着CPU就可以继续执行代码,直接从Buffer中就可以读写数据,而不用亲自与文件或者网络等进行数据的I/O操作。举个例子【参考】:
你带兵打仗,需要军火支应,从仓库搬运军火的数量和批次是不变的,那么你是会一次搬出需要的所有军火前去打仗,还是在打仗的过程中派人搬一箱,没了再搬一箱呢?显然是第一种方法,但是一次全部拿出来,往哪里放?这时就需要一辆卡车(缓冲区)用来存放并运输,这样,一线战士(应用程序)就不用担心弹药不足从而提高作战效率,库房管理者(cpu)也不用担心会有人回来提调军火而一直守在库房旁边,他完全可以去做别的工作了。
原来NIO 用Buffer 提高 IO 的效率, Mina 又封装 NIO 来提高其使用性。其实缓存技术到处存在,数据库缓存,Web应用缓存,CDN等等。
- It doesn't provide useful getters and putters
- It is difficult to write variable-length data due to its fixed capacity
再想想另外一方面, NIO中的 ByteBuffer 与 Byte 又有什么区别,为什么不直接用 Java IO进行读写操作呢?
网上可查,主要是I/O性能的问题,如果直接使用普通I/O进行读写,那么会占用系统的CPU时间,而如果使用Buffer进行I/O操作,那么CPU将会使用DMA方式执行I/O操作,自己去执行其它的操作,等待DMA读取到Buffer中完成发送一个“完成”消息给CPU,接着CPU就可以继续执行代码,直接从Buffer中就可以读写数据,而不用亲自与文件或者网络等进行数据的I/O操作。举个例子【参考】:
你带兵打仗,需要军火支应,从仓库搬运军火的数量和批次是不变的,那么你是会一次搬出需要的所有军火前去打仗,还是在打仗的过程中派人搬一箱,没了再搬一箱呢?显然是第一种方法,但是一次全部拿出来,往哪里放?这时就需要一辆卡车(缓冲区)用来存放并运输,这样,一线战士(应用程序)就不用担心弹药不足从而提高作战效率,库房管理者(cpu)也不用担心会有人回来提调军火而一直守在库房旁边,他完全可以去做别的工作了。
原来NIO 用Buffer 提高 IO 的效率, Mina 又封装 NIO 来提高其使用性。其实缓存技术到处存在,数据库缓存,Web应用缓存,CDN等等。
介绍Mina的IoBuffer之前,先回顾下NIO的ByteBuffer,记住先几个标志的含义:
- position:当前指针的位置,也就是接下来要读写的位置。
- limit:限制,一个缓冲区可读写的范围。
- capability:容量,一个缓冲区最多的存放的字节数。
- mark:标志位,记录当前的位置。
ByteBuffer的几个操作对它们的影响:
- flip():limit=position, position=0.中文意思是“翻转”。
- rewind():position=0,limit不变,可以用于重复读取一段数据. 扩展所有的数据,中文意思是“倒带”,也就是从头开始别的什么也不变。
- clear():position=0,limit=capability,也就是相当于清空了之前的内容,与使用ByteBuffer.allocate(int capacity)是一样的。
- mark( ) 记录当前的位置 mark = position;
- reset( ) position=mark();
IoBuffer及其相关类都在 org.apache.mina.core.buffer 包中,IoBuffer定义了Buffer使用的规则,AbstractIoBuffer提供了具体的实现,后面CachedBufferAllocator中的内部类CachedBuffer继承AbstractIoBuffer并覆盖了某些方法实现,AbstractIoBuffer相当于一个类适配器。
IoBufferAllocator定义了分配管理IoBuffer的接口规则:
public interface IoBufferAllocator {
/**
* Returns the buffer which is capable of the specified size.
*
* @param capacity the capacity of the buffer
* @param direct <tt>true</tt> to get a direct buffer,
* <tt>false</tt> to get a heap buffer.
*/
IoBuffer allocate(int capacity, boolean direct);
/**
* Returns the NIO buffer which is capable of the specified size.
*
* @param capacity the capacity of the buffer
* @param direct <tt>true</tt> to get a direct buffer,
* <tt>false</tt> to get a heap buffer.
*/
ByteBuffer allocateNioBuffer(int capacity, boolean direct);
/**
* Wraps the specified NIO {@link ByteBuffer} into MINA buffer.
*/
IoBuffer wrap(ByteBuffer nioBuffer);
/**
* Dispose of this allocator.
*/
void dispose();
}
上面的方法根据名字很好理解,这里面涉及到Java的两种内存 Direct Buffer(直接缓存) VS. Heap Buffer(堆
缓存)。后者很容易理解,而直接内存却会觉得陌生,网上介绍如下,它们之间的区别可以【
参考】:
- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
- 显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常.
接口 IoBufferAllocator 有两个实现类 SimpleBufferAllocator 和 CachedBufferAllocator. 它们之间的区别在于,当需要频繁修改buffer的大小时,前者会给VM带来很大的压力,因为 SimpleBufferAllocator 是每次都重新分配一个新的buffer, 然后拷贝数据, 最后标记旧的buffer,进行垃圾回收。而CachedBufferAllocator 在多核处理器的并行环境下,会使用 ThreadLocal 存储buffer中数据用于后续的重用,使得扩展buffer时候可以重用。它们的共同点在于:都是对 NIO ByteBuffer 的顶层封装。看看SimpleBufferAllocator:
public class SimpleBufferAllocator implements IoBufferAllocator {
public IoBuffer allocate(int capacity, boolean direct) {
return wrap(allocateNioBuffer(capacity, direct));
}
public ByteBuffer allocateNioBuffer(int capacity, boolean direct) {
ByteBuffer nioBuffer;
if (direct) {
nioBuffer = ByteBuffer.allocateDirect(capacity);
} else {
nioBuffer = ByteBuffer.allocate(capacity);
}
return nioBuffer;
}
public IoBuffer wrap(ByteBuffer nioBuffer) {
return new SimpleBuffer(nioBuffer);
}
public void dispose() {
// Do nothing
}
private class SimpleBuffer extends AbstractIoBuffer {
private ByteBuffer buf;
// ******
}
}
可以看出
SimpleBufferAllocator 最终将会调用 ByteBuffer.allocate() 方法,说明IoBuffer底层还是用 NIO ByteBuffer实现的。同理 CachedBufferAllocator 也是如此。
public class CachedBufferAllocator implements IoBufferAllocator {
private static final int DEFAULT_MAX_POOL_SIZE = 8;
private static final int DEFAULT_MAX_CACHED_BUFFER_SIZE = 1 << 18; // 256KB
private final int maxPoolSize;
private final int maxCachedBufferSize;
private final ThreadLocal<Map<Integer, Queue<CachedBuffer>>> heapBuffers;
private final ThreadLocal<Map<Integer, Queue<CachedBuffer>>> directBuffers;
public CachedBufferAllocator() {
this(DEFAULT_MAX_POOL_SIZE, DEFAULT_MAX_CACHED_BUFFER_SIZE);
}
public CachedBufferAllocator(int maxPoolSize, int maxCachedBufferSize) {
if (maxPoolSize < 0) {
throw new IllegalArgumentException("maxPoolSize: " + maxPoolSize);
}
if (maxCachedBufferSize < 0) {
throw new IllegalArgumentException("maxCachedBufferSize: " + maxCachedBufferSize);
}
this.maxPoolSize = maxPoolSize;
this.maxCachedBufferSize = maxCachedBufferSize;
this.heapBuffers = new ThreadLocal<Map<Integer, Queue<CachedBuffer>>>() {
@Override
protected Map<Integer, Queue<CachedBuffer>> initialValue() {
return newPoolMap();
}
};
this.directBuffers = new ThreadLocal<Map<Integer, Queue<CachedBuffer>>>() {
@Override
protected Map<Integer, Queue<CachedBuffer>> initialValue() {
return newPoolMap();
}
};
}
根据上面的源码可知,CachedBufferAllocator 在多线程环境下,使用两个 ThreadLocal 变量 heapBuffers 和 directBuffers 作为缓存池来存储可重用的 Buffer(不是立即GC)。意识流到ThreadLocal,面试时候遇到过:
ThreadLocal的归纳:
- 每个线程Thread实例都有一个自己的ThreadLocalMap,可以将与该线程相关的对象保存在map中,以ThreadLocal作为Key,这样每个线程都可以访问到自己的Map中Key对应的对象Value,实现了对象的私有化,似乎每一个线程都有属于自己的对象
- 可以避免参数传递的麻烦,如果不用这个方法,要通过参数传递对象的引用。
- ThreadLocal作为Key的Value对象可能就是线程共享的对象,依旧存在共享同步问题。
ThreadLocal应用场景:
适合 在一个线程对应一个或者多个独立的实例的场景中使用,并且这个对象很多地方都要用到,比如如下的一个hibernate中典型的ThreadLocal应用:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
通过上面的方式,使用ThreadLocal,就可以保证每个线程操作自己的Session对象了。ThreadLocal关键部分源码
【
参考
】。
Map<Integer, Queue<CachedBuffer>> newPoolMap() {
Map<Integer, Queue<CachedBuffer>> poolMap = new HashMap<Integer, Queue<CachedBuffer>>();
int poolSize = maxPoolSize == 0 ? DEFAULT_MAX_POOL_SIZE : maxPoolSize;
for (int i = 0; i < 31; i++) {
poolMap.put(1 << i, new ConcurrentLinkedQueue<CachedBuffer>());
}
poolMap.put(0, new ConcurrentLinkedQueue<CachedBuffer>());
poolMap.put(Integer.MAX_VALUE, new ConcurrentLinkedQueue<CachedBuffer>());
return poolMap;
}
上面的方法是初始化 heapBuffers 和 directBuffers 变量时调用的,返回了一个HashMap<Integer, Queue<CachedBuffer>>, 用到了
ConcurrentLinkedQueue。可以发现,它只重用了capacity为2的指数倍大小的buffer,而不是缓存所有大小的capacity, 下面接着看 allocate方法:
public IoBuffer allocate(int requestedCapacity, boolean direct) {
int actualCapacity = IoBuffer.normalizeCapacity(requestedCapacity);
IoBuffer buf;
if ((maxCachedBufferSize != 0) && (actualCapacity > maxCachedBufferSize)) {
if (direct) {
buf = wrap(ByteBuffer.allocateDirect(actualCapacity));
} else {
buf = wrap(ByteBuffer.allocate(actualCapacity));
}
} else {
Queue<CachedBuffer> pool;
if (direct) {
pool = directBuffers.get().get(actualCapacity);
} else {
pool = heapBuffers.get().get(actualCapacity);
}
// Recycle if possible.
buf = pool.poll();
if (buf != null) {
buf.clear();
buf.setAutoExpand(false);
buf.order(ByteOrder.BIG_ENDIAN);
} else {
if (direct) {
buf = wrap(ByteBuffer.allocateDirect(actualCapacity));
} else {
buf = wrap(ByteBuffer.allocate(actualCapacity));
}
}
}
buf.limit(requestedCapacity);
return buf;
}
每次调用allocate方法时,先检查要分配的buffer的大小是否超出了缓存池中可缓存的buffer的大小,如果超出,就直接调用warp(ByteBuffer.allocaot())方法创建一个新的Buffer,而如果没有超出,就从缓存池中找下是否有这个
actualCapacity大小缓存队列,有就直接使用,没有就
调用warp(ByteBuffer.allocaot())方法创建新的Buffer.
到此,已经讲解了Mina---IoBuffer的主要内容,某些极端情况下CachedBufferAllocator会比SimpleBufferAllocator性能好,但是消耗更多的内存,因为使用缓存来减少频繁创建和GC带来的性能消耗,但是大多数情况性能差异不大,网上看到的,个人表示赞同,就像初中物理老师讲
杠杆原理时说的,省力就要费距离,还有就是Chrome浏览器虽然很快,但是内存消耗严重,用内存换速度,天下本来就没有十全十美的事儿,付出才有收获,成功背后不知有多少汗水。