目录
一、复制缓冲区
1.复制一个缓冲区
2.只读缓冲区
3.分割缓冲区
二、字节缓冲区
1.字节顺序
2.直接缓冲区
3.视图缓冲区
三、总结
四、参考资料
五、系列文章阅读提示:可只读标题及红色小结部分。
一、复制缓冲区
1.复制一个缓冲区
CharBuffer buffer = CharBuffer.allocate(10);
char[] chars = {'A','B','C','D','E','F','G','H','I','J'};
buffer.put(chars,0,10);
buffer.position(2).limit(7);
CharBuffer newBuffer = buffer.duplicate(); // @1
buffer.clear(); // @2
System.out.println(newBuffer.get());
@1 newBuffer拥有了与buffer相同的mark、position、limit
@2 buffer执行clear后不会对newBuffer造成影响
Duplicate源码
public CharBuffer duplicate() {
return new HeapCharBuffer(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset); // @1
}
@1 通过new HeapCharBuffer创建新的buffer有自己的mark、position、limit; hb使用了原来buffer的变量char[] hb,即新的buffer共用原来buffer的内存数据
小结:新旧buffer对象共用一份内存数据,拥有各自的mark、position、limit。
2.只读缓冲区
AsReadOnlyBuffer示例
CharBuffer buffer = CharBuffer.allocate(10);
char[] chars = {'A','B','C','D','E','F','G','H','I','J'};
buffer.put(chars,0,10);
buffer.position(2).limit(7);
CharBuffer newBuffer = buffer.asReadOnlyBuffer(); // @1
buffer.clear(); // @2
System.out.println(newBuffer.get());
@1 创建只读缓冲区拥有原缓冲区相同的mark、position、limit
@2 原缓冲区的清理不会影响新缓冲区
AsReadOnlyBuffer源码
public CharBuffer asReadOnlyBuffer() {
return new HeapCharBufferR(hb, // @1
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
public CharBuffer put(char x) {
throw new ReadOnlyBufferException(); // @2
}
public CharBuffer put(int i, char x) {
throw new ReadOnlyBufferException(); // @2
}
@1 构建只读缓冲区对象HeapCharBufferR;HeapCharBufferR与HeapCharBuffer的区别isReadOnly=true
@2 只读缓冲区在使用put操作时,会抛出ReadOnlyBufferException
小结:构建只读缓冲区实例对象为HeapCharBufferR,只读缓冲区拥有独立的mark、position、limit。
3.分割缓冲区
Slice示例
CharBuffer buffer = CharBuffer.allocate(10);
char[] chars = {'A','B','C','D','E','F','G','H','I','J'};
buffer.put(chars,0,10);
buffer.position(2).limit(7);
CharBuffer newBuffer = buffer.slice(); // @1
buffer.clear();
System.out.println(newBuffer.get());
@1 newBuffer是一个mark=-1、position=0、limit=capacity=5的缓冲区
Slice示例运行内存截图
Slice源码
public CharBuffer slice() {
return new HeapCharBuffer(hb, // @1
-1, // @2
0, // @3
this.remaining(), // @4
this.remaining(), // @5
this.position() + offset);
}
@1 与原缓冲区共用内存数据hb
@2 mark重置为-1
@3 position重置为0
@4 limit设置为剩余元素(原limit-原position)
@5 capacity设置为剩余元素
小结:通过slice()创建的缓冲区是一个mark=-1、position=0、limit=capacity=(原limit-原position)的子缓冲区。
二、字节缓冲区
系统层面的I/O是面向字节的。字节是操作系统及其I/O设备使用的基本数据类型,当JVM与操作系统之间数据交付时,将其他数据类型拆分成字节。
1.字节顺序
字节顺序:多字节数值被存储在内存中的方式
大端顺序:高字节数据存放在低地址处,低字节数据存放在高地址处
小端顺序:低字节数据存放在内存低地址处,高字节数据存放在内存高地址处
字节示例
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
System.out.println(byteBuffer.order()); // @1
System.out.println(ByteOrder.nativeOrder()); // @2
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // @3
System.out.println(byteBuffer.order()); // @4
@1 输出BIG_ENDIAN;ByteBuffer默认大端顺序
@2 输出LITTLE_ENDIAN;本地为小端顺序
@3 修改ByteBuffer为小端顺序
@4 输出LITTLE_ENDIAN;ByteBuffer的字节顺序已修改
ByteBuffer源码
boolean bigEndian = true; // @1
boolean nativeByteOrder = (Bits.byteOrder() == ByteOrder.BIG_ENDIAN);
public final ByteOrder order() {
return bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
}
public final ByteBuffer order(ByteOrder bo) {
bigEndian = (bo == ByteOrder.BIG_ENDIAN);
nativeByteOrder =
(bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));
return this;
} // @2
@1 从ByteBuffer的源代码可以看出默认使用大端字节顺序
@2 修改ByteBuffer的字节顺序小结:ByteBuffer默认使用大端顺序,可以通过order(ByteOrder bo)进行修改;网络通信时,当本地主机字节顺序和通用的网络字节顺序不一致需要转换。
2.非直接缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(10); // @1
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0); // @2
}
@1 创建非直接缓冲区
@2 非直接缓冲区通过new byte[cap]在堆空间分配内存小结:非直接缓冲区在堆空间分配内存;JVM运行在用户空间。
3.直接缓冲区
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(10);
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize(); // @1
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap); // @2
long base = 0;
try {
base = unsafe.allocateMemory(size); // @3
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0); // @4
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;
}
@1 pageSize通常为4k
@2 判断堆外内存是否有足够的空间分配;可以通过-XX:MaxDirectMemorySize来设置;默认为64M(directMemory=67108864L)
@3 分配堆外内存空间;返回在内存中的地址
@4 初始化内存空间
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
long address = unsafe.allocateMemory(10);
unsafe.setMemory(address, 10, (byte) 0);
System.out.println(unsafe.pageSize());
System.out.println(address);
小结:由源码可以看出直接缓冲区在堆外分配内存,不受堆内存大小限制;用户空间和内核空间可以通过虚拟内存地址对直接内存进行访问;减少用户空间与内核空间的拷贝。
4.视图缓冲区
I/O本质上是字节的传递,ByteBuffer提供方便的API创建视图缓冲区。
1. 通过工厂方法创建视图缓冲区,有自己独立的属性、容量、位置、上界和标记。例如:上文中复制缓冲区和分割缓冲区。 2.通过ByteBuffer提供API映射为基本类型缓冲区。例如:asCharBuffer()、asLongBuffer()等通过工厂类创建视图缓冲区见上文复制/分割缓冲区源码部分,下面以asCharBuffer()分析下如何转换的。
代码示例
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
charBuffer.put('A');
小结:创建字节缓冲区后转换为CharBuffer,然后填充字符A。
源码分析
public CharBuffer asCharBuffer() {
int size = this.remaining() >> 1;
int off = offset + position();
return (bigEndian
? (CharBuffer)(new ByteBufferAsCharBufferB(this,
-1,
0,
size,
size,
off))
: (CharBuffer)(new ByteBufferAsCharBufferL(this,
-1,
0,
size,
size,
off))); // @1
}
ByteBufferAsCharBufferB(ByteBuffer bb,
int mark, int pos, int lim, int cap,
int off){
super(mark, pos, lim, cap);
this.bb = bb; // @2
offset = off;
}
public CharBuffer put(char x) {
Bits.putCharB(bb, ix(nextPutIndex()), x); // @3
return this;
}
static void putCharB(ByteBuffer bb, int bi, char x) {
bb._put(bi , char1(x)); // @4
bb._put(bi + 1, char0(x)); // @4
}
private static byte char1(char x) { return (byte)(x >> 8); } // @4
private static byte char0(char x) { return (byte)(x ); } // @4
@1 由于大端和小端字节顺序不通,位移方向不同;由不通的类实现,以大端为例分析
@2 ByteBufferAsCharBufferB中封装的是字节缓冲 即:ByteBuffer bb
@3 向charBuffer中写入字符。例如例子中的字符'A'
@4 通过为位操作将char转换为byte;存入ByteBuffer bb中
小结:ByteBuffer映射为基本数据缓冲区后,其内部依然为字节缓冲区ByteBuffer;当写入和取出时通过基本类型(例如:char)与byte互相转换来实现。
三、总结
本文从源码角度跟踪分析了复制缓冲区、只读缓冲区、分割缓冲区、字节顺序、非直接缓冲区、直接缓冲区、视图缓冲区的实现原理。
四、参考资料
《Java NIO》第二章(完)