Netty4和Netty5内存池的使用心得

一、为什么要使用内存池?

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。而且这些实例随着消息的处理朝生夕灭,这就会给服务器带来沉重的GC压力,同时消耗大量的内存。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。性能测试表明,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

二、如何启动并初始化内存池?

在Netty4或Netty5中实现了一个新的ByteBuf内存池,它是一个纯Java版本的 jemalloc (Facebook也在用)。现在,Netty不会再因为用零填充缓冲区而浪费内存带宽了。 不过,由于它不依赖于GC,开发人员需要小心内存泄漏。如果忘记在处理程序中释放缓冲区,那么内存使用率会无限地增长。 Netty默认不使用内存池,需要在创建客户端或者服务端的时候在引导辅助类中进行配置:

                                // option是boss线程配置,childOption是work线程配置.
				EventLoopGroup bossGroup = new NioEventLoopGroup();
				EventLoopGroup workerGroup = new NioEventLoopGroup();
					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();

三、如何在自己的业务代码中使用内存池?

                     首先,介绍一下Netty的ByteBuf缓冲区的种类:ByteBuf支持堆缓冲区和堆外直接缓冲区,根据经验来说,底层IO处理线程的缓冲区使用堆外直接缓冲区,减少一次IO复制。业务消息的编解码使用堆缓冲区,分配效率更高,而且不涉及到内核缓冲区的复制问题。

ByteBuf的堆缓冲区又分为内存池缓冲区PooledByteBuf和普通内存缓冲区UnpooledHeapByteBuf。PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。在高并发的情况下推荐使用PooledByteBuf,可以节约内存的分配。在性能能够保证的情况下,可以使用UnpooledHeapByteBuf,实现比较简单。

在此说明这是当我们在业务代码中要使用池化的ByteBuf时的方法

第一种情况:若我们的业务代码只是为了将数据写入ByteBuf中并发送出去,那么我们应该使用堆外直接缓冲区DirectBuffer.使用方式如下:

public class TestEncode extends MessageToByteEncoder {
	@Override
	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.使用方式如下: 
  

                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纳秒,如图: 

Netty4和Netty5内存池的使用心得_第1张图片

当使用buffer.readBytes(Byte[] context)时,占用时间9741纳秒,如图: 

Netty4和Netty5内存池的使用心得_第2张图片

之间相差了547倍的速度。

为什么速度差距如此之大,那么我们看下Netty的源码便知,如图:

buffer.readBytes(int length)方法的实现:

Netty4和Netty5内存池的使用心得_第3张图片

buffer.readBytes(Byte[] context)方法的实现:


所以,总体来说,使用内存池后,应用的性能会大幅提升,但是编码难度从而也会提升,鱼和熊掌往往是不可兼得的,么么哒!











你可能感兴趣的:(Netty)