Java nio — ByteBuffer
缓冲区本质是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包裹成NIO buffer对象,并且提供了一些方法来访问该块内存。
1.ByteBuffer的分配
ByteBuffer是一个抽象类,所以我们不能直接通过new 来创建我们需要的缓冲对象,当然也不用通过重写抽象方法来创建对象。
ByteBuffer内部提供了两个静态方法来帮助我们创建内存:
分别是分配HeapByteBuffer的:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
分配DirectByteBuffer的:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
都是分配内存,为什么需要两种不同的方法呢?其实我们从他们的名字就能看出来,前者分配内存的区域是在Heap区域,当flush到远程的时候会拷贝到直接内存。而后者则是在C heap上分配内存,可以得到非常快速的网络交互。
HeapByteBuffer和DirectByteBuffer都是包内私有,无法被我们访问的。只能通过ByteBuffer间接访问。
通过源码,我们能看到创建实例的方法如下:
HeapByteBuffer:
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
该缓冲区依旧委托给父类进行创建。除了调用父类构造方法,也没有其他的操作。
我们来看一看创建一块ByteBuffer需要哪些参数:
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;
}
还记得吗?我们在调用allocate()方法的时候仅仅向里面传递了一个cap。
而HeapByteBuffer传回将lim = cap, pos = 0, offset = 0 mark = -1 (即 undefine)
这几个参数一会儿我们来讨论其意义。
继续向上调用构造函数
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的构造函数,其实现类很多,在此不论。
创建完成之后,此时内存空间重要参数布局是这样的:
bt1.png
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;
}
由于该缓冲区是堆外申请的内存,我们可以看到除了调用父类的构造函数,还进行了很多其他操作。
该类继承自MappedByteBuffer类,关于该类的具体资料:
http://blog.csdn.net/linxdcn/article/details/72903422
这些额外的操作主要就要获取address(堆外内存地址),还有就是直接内存不是分配在JVM堆中的,而是用JNI直接调用的内存。并不受Minor GC的影响,只有当老年代执行Full gc的时候才会回收直接内存。而只有当众多DirectByteBuffer被送入老年代后才会触发Full gc。
关于堆外内存的回收机制:
http://www.importnew.com/26334.html
分配后内存参数和HeapByteBuffer相同。
这两种内存空间模型如下:
HeapByteBuffer
JVM Heap <----> JVM用户空间 <----> OS内核空间<----->网卡驱动空间;
DirectByteBuffer
JVM用户空间 <----> OS内核空间<----->网卡驱动空间。
directBytebuffer虽然看似要快许多,但在数据量较少的时候并无太大的优势,而且内存需要手动释放,容易出现问题。
所以如果没有性能瓶颈尽可能使用HeapByteBuffer来作为缓冲区。
2.ByteBuffer的读写及参数意义:
主要参数有:
- capacity
- position
- limit
-
mark
关于bytebuffer的读写原理见插图:
image
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
除了以上方法外,还能使用类似bytebuffer.put(127)向缓冲区中写数据。
了解了这几个参数的意义后,我们来看看几个重要方法的
flip()方法
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
flip()方法应该是最常用的方法,因为bytebuffer的结构,在读数据的时候,我们会将pos = 0, limit = pos,这个意思就是我们可以并且只能读取我们之前写入的数据。
clear()方法
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
如源码所示,将一切属性设置为来初始化状态。
rewind()方法
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
将pos=0,可以重新读取所有内容。
remain()方法
public final int remaining() {
return limit - position;
}
读写模式下表示可读/可写范围。
compact()
方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
由于此处涉及到分配内存,所以HeapByteBuffer和DirectByteBuffer的实现又不一样。详情见源码。
在前面我们已经讨论过了pos, lim, cap三个参数,还有一个mark参数未讨论。
看其字面意思,其实就是标记的意思。
mark()方法
public final Buffer mark() {
mark = position;
return this;
}
将mark=pos.
该参数需要配合
reset()方法
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
当我们需要记住前一个pos的时候(此时,我们已经调用了rewind或者clear了),可以使用mark参数。
equals()与compareTo()方法
equals()
当满足下列条件时,表示两个Buffer相等:
有相同的类型(byte、char、int等)。
Buffer中剩余的byte、char等的个数相等。
Buffer中所有剩余的byte、char等都相同。
如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
compareTo()方法
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
第一个不相等的元素小于另一个Buffer中对应的元素 。
所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
剩余元素 : pos -> limit
参考资料: