Mina-2.0.7源码学习 (3) ------ IoBuffer

    Mina中的 IoBuffer 主要是对 Java NIO 中 ByteBuffer 的封装和替换,为什么不直接使用NIO中的ByteBuffer呢?Mina的解释

  • It doesn't provide useful getters and putters
  • It is difficult to write variable-length data due to its fixed capacity
    说起来就是Java NIO中的 ByteBuffer 容量固定不易扩展并且使用不方便,它只是提供了原生的 Buffer接口 操作,原来如此,那 IoBuffer 就扩展一下呗。
    再想想另外一方面, 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:标志位,记录当前的位置。

    界限是用来控制当前读写的范围,如果容量为100,界限为70,则位置只能在0-70之间,即只能读写0-70之间的数据。

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(堆 缓存)。后者很容易理解,而直接内存却会觉得陌生,网上介绍如下,它们之间的区别可以【 参考】:
  1. 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  2. 显然,本机直接内存的分配不会受到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的归纳:
  1.     每个线程Thread实例都有一个自己的ThreadLocalMap,可以将与该线程相关的对象保存在map中,以ThreadLocal作为Key,这样每个线程都可以访问到自己的Map中Key对应的对象Value,实现了对象的私有化,似乎每一个线程都有属于自己的对象
  2.     可以避免参数传递的麻烦,如果不用这个方法,要通过参数传递对象的引用。
  3.     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浏览器虽然很快,但是内存消耗严重,用内存换速度,天下本来就没有十全十美的事儿,付出才有收获,成功背后不知有多少汗水。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值