一、Buffer(缓冲区)
- 一个用于特定基本数据类型的容器。定义子java.nio包中,所有的缓冲区都是抽象类Buffer的子类。
- Java NIO中的Buffer主要用于与NIO通道(channel)进行交互,数据是从通道读入缓冲区,或从缓冲区写入通道。
- 负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据。
根据数据类型不同(boolean除外),提供了相应类型的缓冲区:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
1.上述缓冲区都继承自抽象类Buffer,管理方式几乎一致,通过allocate()获取缓冲区。
2.缓冲区中的四个核心属性:
- capacity:容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。
- limit:界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)
- position:位置,表示缓冲区中正在操作数据的位置。
- mark:标记,表示记录当前position的位置(初始时为-1)。可以通过reset()恢复到mark的位置。
public abstract class Buffer {
/**
* The characteristics of Spliterators that traverse and split elements
* maintained in Buffers.
*/
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
如果没有调用过mark方法,则在reset()的时候mark由于等于-1,会抛出InvalidMarkException。
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
3.缓冲区存取数据的两个核心方法:
- put():存入数据到缓冲区中
-
get():获取缓冲区中的数据
image.png
4.缓冲区操作的几个重要方法(这些操作全部都是对limit,position,capacity和mark四个指针中的某几个进行的操作,而没有改变实际存储的数据):
mark():标记,表示记录当前position的位置。可以通过reset()恢复到mark的位置
flip():切换到读取数据模式。
rewind():可重复读(将读指针position重置为0)
clear():清空缓冲区。但是缓冲区中的数据依然存在,但是处于“被遗忘状态”。(将capacity,limit,position几个指针重新初始化)
hasRemaining():判断缓冲区中是否还有可用数据
remaining():获取缓冲区中可用数据的数量
@Test
public void test1() {
String str = "abcde";
//1.分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("-------------allocate()--------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// -------------allocate()--------------
// 0
// 1024
// 1024
//2.利用put()存入数据到缓冲区中
buf.put(str.getBytes());
System.out.println("-------------put()--------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// -------------put()--------------
// 5
// 1024
// 1024
//3.切换到读取数据模式
buf.flip();
System.out.println("-------------flip()--------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// -------------flip()--------------
// 0
// 5
// 1024
// abcde
//4.利用get()读取缓冲区中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst, 0, dst.length));
System.out.println("-------------get()--------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// -------------get()--------------
// 5
// 5
// 1024
//5.rewind():可重复读(将读指针position重置为0)
buf.rewind();
System.out.println("-------------rewind()--------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// -------------rewind()--------------
// 0
// 5
// 1024
// a
//6.clear():清空缓冲区。但是缓冲区中的数据依然存在,但是处于“被遗忘状态”。(将capacity,limit,position几个指针重新初始化)
System.out.println("-------------clear()--------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
System.out.println((char) buf.get());
// -------------clear()--------------
// 0
// 5
// 1024
// a
}
@Test
public void test2() {
String str = "abcde";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2);
System.out.println(new String(dst, 0, 2));
System.out.println(buf.position());
//mark():标记
buf.mark();
buf.get(dst, 2, 2);
System.out.println(new String(dst, 2, 2));
System.out.println(buf.position());
//reset ():恢复到mark的位置
buf.reset();
System.out.println(buf.position());
//判断缓冲区中是否还有剩余的数据
if (buf.hasRemaining()) {
//获取缓冲区中可以操作的数量
System.out.println(buf.remaining());
}
}
二、直接与非直接缓冲区
- 直接缓冲区的使用能够避免将缓冲区的内容复制到中间缓冲区中或者从中间缓冲区中复制内容,即通常所说的0拷贝。如果分配为直接缓冲区,则Java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作,即其内容会在Java的堆之外物理内存进行存储。关于非直接缓冲区与缓冲区的对比如下图所示:
非直接缓冲区
直接缓冲区
直接缓冲区优点
直接缓冲区能够减少内核地址空间与用户地址空间数据拷贝的消耗,提高程序的执行效率。
直接缓冲区缺点
在物理内存中开辟这样一块空间需要消耗更多的资源,并且该内存资源的释放需要借助Java的垃圾回收机制进行资源的回收。因此,若要使用直接缓冲区,通常这些数据需要长时间留在内存并经常进行操作,且使用直接缓冲区确实能带来效率的提升的情况下才会去使用它。
直接缓冲区使用方法
1.可以调用Buffer类的allocateDirect()工厂方法来创建,此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
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;
}
可以看到上述过程最终调用了unsafe.allocateMemory(size)方法,这是一个native方法去开辟物理内存
2.可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建,该方法返回MappedByteBuffer。
3.缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。
@Test
public void test3() {
//分配直接缓冲区
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
//true
System.out.println(buf.isDirect());
//分配非直接缓冲区
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//false
System.out.println(buf2.isDirect());
}
网友评论