Buffer的意思就是缓冲区,它的作用就是在内存中预留出一定空间的内存大小,主要用来作为临时数据的存储,那么这部分内存区域,我们就称之为缓冲区,这样做的好处有俩个:
1、减少实际的物理读写次数
2、缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数
比如,现在有一批货需要从A地移到B地,有100个货物,B地虽然有很大的位置给A的货物放置,但是不知道A具体需要放多少东西,那只能一次给一个货物的位置,那么A拉货的时候就很烦了,因为B告诉A你每次只能拉一个东西过来,因为我不知道你要拉多少东西,我位置不能乱给,那么A就苦逼了,要拉100次,而且每次都要B重新给了货物的位置,那么最终的结果就是A和B都很烦,那现在有个方案了,A告诉B,我现在要50个货物的位置,你给我留着,我肯定能用上,B说,好啊,那你拉吧,这下A开心了,一次就拉了50个货物过去,结果只要拉俩次就把货物拉完了,A和B都很开心。
那上面的例子就是能够反映出缓冲区的作用,预留货物的位置就可以称之为缓冲区。
那么再来看Buffer,Buffer本身是抽象类,我们看它的子类。
它的子类有:
ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer
那我们选ByteBuffer看,一看,我擦,还是抽象的,但是不要紧,ByteBuffer里面的一些方法基本上就可以让我们知道缓冲区是如何使用的。
那下面说说关于它的一些介绍。
Fields
属性 | 描述 |
---|---|
Capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 |
Mark | 标记,调用mark()来设置mark=position,再调用reset()可以让position恢复到标记的位置 |
实例化
ByteBuffer类提供了4个静态工厂方法来获得ByteBuffer的实例:
方法 | 描述 |
---|---|
allocate(int capacity) | 从堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器 |
allocateDirect(int capacity) | 是不使用JVM堆栈而是通过操作系统来创建内存块用作缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并长期存在,或者需要经常重用时,才使用这种缓冲区 |
wrap(byte[] array) | 这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方。其实ByteBuffer底层本来就有一个bytes数组负责来保存buffer缓冲区中的数据,通过allocate方法系统会帮你构造一个byte数组 |
wrap(byte[] array,int offset, int length) | 在上一个方法的基础上可以指定偏移量和长度,这个offset也就是包装后byteBuffer的position,而length呢就是limit-position的大小,从而我们可以得到limit的位置为length+position(offset) |
Methods
方法 | 描述 |
---|---|
limit(), limit(10)等 | 其中读取和设置这4个属性的方法的命名和jQuery中的val(),val(10)类似,一个负责get,一个负责set |
reset() | 把position设置成mark的值,相当于之前做过一个标记,现在要退回到之前标记的地方 |
clear() | position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底层byte数组的内容 |
flip() | limit = position;position = 0;mark = -1; 翻转,也就是让flip之后的position到limit这块区域变成之前的0到position这块,翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态 |
rewind() | 把position设为0,mark设为-1,不改变limit的值 |
remaining() | return limit - position; 返回limit和position之间相对位置差 |
hasRemaining() | return position < limit返回是否还有未读内容 |
compact() | 把从position到limit中的内容移到0到limit-position的区域内,position和limit的取值也分别变成limit-position、capacity。如果先将positon设置到limit,再compact,那么相当于clear() |
get() | 相对读,从position位置读取一个byte,并将position+1,为下次读写作准备 |
get(int index) | 绝对读,读取byteBuffer底层的bytes中下标为index的byte,不改变position |
get(byte[] dst, int offset, int length) | 从position位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域 |
put(byte b) | 相对写,向position的位置写入一个byte,并将postion+1,为下次读写作准备 |
put(int index, byte b) | 绝对写,向byteBuffer底层的bytes中下标为index的位置插入byte b,不改变position |
put(ByteBuffer src) | 用相对写,把src中可读的部分(也就是position到limit)写入此byteBuffer |
put(byte[] src, int offset, int length) | 从src数组中的offset到offset+length区域读取数据并使用相对写写入此byteBuffer |
那说了这么多方法的使用后,我们可以去看一看这个预留内存到底是怎么安排的,查看byteBuffer,我们可以发现有一个变量hb,是final byte[] hb,其余的变量都是其余基本数据类型,应该不是我们想要的,那么围绕这个查找,在put方法里面我们可以查看这样一段代码
if (this.hb != null && src.hb != null) {
System.arraycopy(src.hb, src.position() + src.offset, hb, position() + offset, n);
} else {
final Object srcObject = src.isDirect() ? src : src.hb;
int srcOffset = src.position();
if (!src.isDirect()) {
srcOffset += src.offset;
}
final ByteBuffer dst = this;
final Object dstObject = dst.isDirect() ? dst : dst.hb;
int dstOffset = dst.position();
if (!dst.isDirect()) {
dstOffset += dst.offset;
}
Memory.memmove(dstObject, dstOffset, srcObject, srcOffset, n);
}
在这里我们可以看到会对hb是否为空进行判断,如果不为空执行
System.arraycopy(src.hb, src.position() + src.offset, hb, position() + offset, n);
如果为空,则执行下面长长的代码,一般的代码不必多说,看这一行代码
Memory.memmove(dstObject, dstOffset, srcObject, srcOffset, n);
整个代码片段里面我们可以从这俩个方法,但是其实这俩个方法都是底层实现的,那这里我们解释下具体作用是什么。
System.arraycopy()
首先说个知识前提,Java数组的复制操作可以分为深度复制和浅度复制,简单来说深度复制,可以将对象的值和对象的内容复制;浅复制是指对对象引用的复制。
System.arraycopy的函数原型是
public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
其中,src表示源数组,srcPos表示源数组要复制的起始位置,desc表示目标数组,length表示要复制的长度,那么对于一维数组来说,这种复制属性值传递,修改副本不会影响原来的值。对于二维或者一维数组中存放的是对象时,复制结果是一维的引用变量传递给副本的一维数组,修改副本时,会影响原来的数组。这个各位看官可以自己去测试一下,这里就不再举例了。
Memory.memmove()
memcpy和memmove()都是C语言中的库函数,在头文件string.h中,作用是拷贝一定长度的内存的内容,原型分别如下
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。
我们看看这俩个方法的具体实现
void* my_memcpy(void* dst, const void* src, size_t n)
{
char *tmp = (char*)dst;
char *s_src = (char*)src;
while(n--) {
*tmp++ = *s_src++;
}
return dst;
}
从实现中可以看出memcpy()是从内存左侧一个字节一个字节地将src中的内容拷贝到dest的内存中
再看看另一个方法
void* my_memmove(void* dst, const void* src, size_t n)
{
char* s_dst;
char* s_src;
s_dst = (char*)dst;
s_src = (char*)src;
if(s_dst>s_src && (s_src+n>s_dst)) { //内存覆盖的情形。
s_dst = s_dst+n-1;
s_src = s_src+n-1;
while(n--) {
*s_dst-- = *s_src--;
}
}else {
while(n--) {
*s_dst++ = *s_src++;
}
}
return dst;
}
那么我们再看看内存覆盖是什么情况
从内存可能覆盖的情况来看,当使用memcpy的时候,这种实现方式导致了对于图中第二种内存重叠情形下,最后两个字节的拷贝值明显不是原先的值了,新的值是变成了src的最开始的2个字节了,而memmove是可以正常工作的。
到这里关于Buffer的介绍就结束了,最后感谢以下文章的提供者。
ByteBuffer常用方法详解
System.arraycopy()方法详解
memmove 和 memcpy的区别以及处理内存重叠问题
喜欢技术研究的可以加入QQ群一起讨论:561176094