一、为什么要使用内存池?
随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。而且这些实例随着消息的处理朝生夕灭,这就会给服务器带来沉重的GC压力,同时消耗大量的内存。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。性能测试表明,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。
二、如何启动并初始化内存池
在Netty4.0以上版本中中实现了一个新的ByteBuf内存池,它是一个纯Java版本的 jemalloc (Facebook也在用)。现在,Netty不会再因为用零填充缓冲区而浪费内存带宽了。 不过,由于它不依赖于GC,开发人员需要小心内存泄漏。如果忘记在处理程序中释放缓冲区,那么内存使用率会无限地增长。 Netty默认不使用内存池,需要在创建客户端或者服务端的时候在引导辅助类中进行配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 public static void main(String[] args) {
//option是boss线程配置,childOption是work线程配置.
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 512)
.childOption(ChannelOption.TCP_NODELAY, true)
//Boss线程内存池配置.
.option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT)
//Work线程内存池配置.
.childOption(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT);
ChannelFuture f = server.bind(port).sync();
f.channel().closeFuture().sync();
}
三、如何在自己的业务代码中使用内存池
首先,介绍一下Netty的ByteBuf缓冲区的种类:ByteBuf支持堆缓冲区和堆外直接缓冲区,根据经验来说,底层IO处理线程的缓冲区使用堆外直接缓冲区,减少一次IO复制。业务消息的编解码使用堆缓冲区,分配效率更高,而且不涉及到内核缓冲区的复制问题。
ByteBuf的堆缓冲区又分为内存池缓冲区PooledByteBuf和普通内存缓冲区UnpooledHeapByteBuf。PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。在高并发的情况下推荐使用PooledByteBuf,可以节约内存的分配。在性能能够保证的情况下,可以使用UnpooledHeapByteBuf,实现比较简单。
在此说明这是当我们在业务代码中要使用池化的ByteBuf时的方法:第一种情况:若我们的业务代码只是为了将数据写入ByteBuf中并发送出去,那么我们应该使用堆外直接缓冲区DirectBuffer.使用方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class TestEncode extends MessageToByteEncoder<BasePropertyEntity> {
protected void encode(ChannelHandlerContext ctx, BasePropertyEntity msg,ByteBuf outBuffer) throws Exception {
int length = 10;
//在此使用堆外缓冲区是为了将数据更快速的写入内核中,如果使用堆缓冲区会多一次堆内存向内核进行内存拷贝,这样会降低性能。
ByteBuf buffer = PooledByteBufAllocator.DEFAULT.directBuffer(length);
try {
byte[] context = new byte[length];
buffer.writeBytes(context);
outBuffer.writeBytes(buffer);
} finally {
// 必须释放自己申请的内存池缓冲区,否则会内存泄露。
//outBuffer是Netty自身Socket发送的ByteBuf系统会自动释放,用户不需要做二次释放。
buffer.release();
}
}
}
第二种情况:若我们的业务代码中需要访问ByteBuf中的数组时,那么我们应该使用堆缓冲区HeapBuffer.使用方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13 byte[] context = new byte[10];
int length = 10;
// 在此使用堆缓冲区是为了更快速的访问缓冲区内的数组,如果使用堆外缓冲区会多一次内核向堆内存的内存拷贝,这样会降低性能。
ByteBuf buffer = PooledByteBufAllocator.DEFAULT.heapBuffer(length);
try {
buffer.writeBytes(context);
// 高效率访问堆缓冲区的方式,具体原因随后会讲。
byte[] dst = new byte[10];
buffer.readBytes(dst);
} finally {
// 使用完成后一定要记得释放到内存池中。
buffer.release();
}
四、高效率访问堆缓冲区数组的方式
经过测试,当使用内存池时,要读去ByteBuf中的数组时:
谨慎使用buffer.readBytes(int length),推荐使用buffer.readBytes(Byte[] context),当在读取数组的时候,使用readBytes(int length)时,占用时间为5337387纳秒,如图:
当使用buffer.readBytes(Byte[] context)时,占用时间9741纳秒,如图:
之间相差了547倍的速度。
为什么速度差距如此之大,那么我们看下Netty的源码便知,如图:
buffer.readBytes(int length)方法的实现:
buffer.readBytes(Byte[] context)方法的实现:
所以,总体来说,使用内存池后,应用的性能会大幅提升,但是编码难度从而也会提升,鱼和熊掌往往是不可兼得的,么么哒!
- 本文作者: GreatGarlic
- 本文链接: https://greatgarlic.github.io/2016/05/19/netty_1/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!