5. netty的内存管理
5.1 ByteBuffer和ByteBuf
Java NIO为了减少频繁的IO操作,引入了ByteBuffer,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O 。ByteBuffer具有4个重要的属性:mark、position、limit、capacity ,以及两个重要的方法clear()、flip()
public abstract class Buffer { private int mark = -1; private int position = 0; private int limit; private int capacity; ...}
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> { ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; } ByteBuffer(int mark, int pos, int lim, int cap) { // package-private this(mark, pos, lim, cap, null, 0); } public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw createCapacityException(capacity); return new HeapByteBuffer(capacity, capacity); } ... }
ByteBuffer分配的时候长度固定,一旦分配完成后,容量不能动态扩展,并且ByteBuffer只有一个标识控制位position,写之前要手动调用clear(), 读之前要手动调用flip().
Netty自行封装了ByteBuf,增加了两个指针 readerIndex 和 writeIndex 来分别指向读的位置和写的位置,不需要每次为读写做准备,直接设置读写指针进行读写操作即可。
public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { public ByteBuf() { } public abstract int readerIndex(); public abstract ByteBuf readerIndex(int var1); public abstract int writerIndex(); public abstract ByteBuf writerIndex(int var1); ... }
而且clear和flip方法仅仅是设置writerIndex和readerIndex的值,可以根据需要重复利用内存空间。而且可以控制读内容的大小。
5.2 netty的zeroCopy
Netty的零拷贝体现在三个方面:
1. Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {... 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; } ... }
public static void main(String[] args) {
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(100);
System.out.println(byteBuf.capacity());
byte[] data = "this is test".getBytes();
byteBuf.writeBytes(data);
System.out.println(byteBuf.readerIndex());
System.out.println(byteBuf.writerIndex());
// 观察内存占用大小
ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(1024*1024*1024);
System.out.println(directBuf.capacity());
}
2. Netty提供了组合Buffer对象Composite Buffers,可以聚合多个ByteBuffer对象。传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers) {
this.addComponents0(increaseWriterIndex, this.components.size(), buffers, 0, buffers.length);
this.consolidateIfNeeded();
return this;
}
public static void main(String[] args) {
ByteBuf byteBuf1 = ByteBufAllocator.DEFAULT.directBuffer(100);
byte[] dataStr1 = "this is test buf1".getBytes();
byteBuf1.writeBytes(dataStr1);
ByteBuf byteBuf2 = ByteBufAllocator.DEFAULT.directBuffer();
byte[] dataStr2 = "this is test buf2. hahahaha".getBytes();
byteBuf1.writeBytes(dataStr2);
CompositeByteBuf byteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
System.out.println(byteBuf.capacity());
byteBuf.addComponents(byteBuf1, byteBuf2);
System.out.println(byteBuf.capacity());
byte[] data = new byte[dataStr1.length+dataStr2.length];
byteBuf.getBytes(0, data);
System.out.println(new String(data));
}
3. Unpooled.wrappedBuffer
Unpooled.wrappedBuffer 方法可以将不同的数据源的一个或者多个数据包装成一个大的 ByteBuf 对象,包装的过程中不会发生数据拷贝操作,包装后生成的 ByteBuf 对象和原始 ByteBuf 对象是共享底层的 byte 数组。
public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers) {
switch (buffers.length) {
case 0:
break;
case 1:
if (buffers[0].hasRemaining()) {
return wrappedBuffer(buffers[0].order(BIG_ENDIAN));
}
break;
default:
List<ByteBuf> components = new ArrayList(buffers.length);
ByteBuffer[] var3 = buffers;
int var4 = buffers.length;
// 利用CompositeByteBuf,将底层的byteBuf组合起来
for(int var5 = 0; var5 < var4; ++var5) {
ByteBuffer b = var3[var5];
if (b == null) {
break;
}
if (b.remaining() > 0) {
components.add(wrappedBuffer(b.order(BIG_ENDIAN)));
}
}
if (!components.isEmpty()) {
return new CompositeByteBuf(ALLOC, false, maxNumComponents, components);
}
}
return EMPTY_BUFFER;
}
4. ByteBuf.slice
ByteBuf.slice 和 Unpooled.wrappedBuffer 的逻辑相反,是将一个 ByteBuf 对象切分成多个共享同一个底层存储的 ByteBuf 对象。原理是AbstractUnpooledSlicedByteBuf.adjustment可以调整buf的起始位置。
5. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。