Netty 系列笔记之内存管理
一、引文
对于 Java 程序来说,通过合理的内存使用,减少 Full GC 的 STW 时间对于程序来说可以获得更好的性能。本文结合 Netty 来看如何对 Java 内存更合理的使用。
二、内存使用的目标
前提:尽可能的占用内存更少
预期:获得更快的程序执行速度
于 Java 而言:减少 Full GC 的 STW 时间。
三、内存使用技巧
1、减少对象本身的大小
使用基本类型而不是包装类型
包装类型相比较基本类型而言多了 object header ,会占用更多的内存。使用 static 类变量而不是实例变量
一般如果类是非单例的,会有多个实例,使用类变量会节省更多的内存。❤ Netty 用于统计等待写的请求的字节数
io.netty.channel.ChannelOutboundBuffer
private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");("UnusedDeclaration")private volatile long totalPendingSize;
Netty 使用 static AtomicLongFieldUpdater 与 volatile long 结合的形式,减少本对象的内存占用。其中 AtomicLongFieldUpdater 采用反射的形式原子的更新本类中 volatile long 类型的变量。
2、对内存分配预估
HashMap 在超过容量的 0.75 时会扩容为 2 倍,对于可以预知容量的 HashMap 指定 size 避免库容浪费空间。
❤ Netty 根据接收到的数据动态调整下一个要分配 Buffer 的大小
io.netty.channel.AdaptiveRecvByteBufAllocator#record(int actualReadBytes)
private void record(int actualReadBytes) {// 尝试是否可以减小分配的空间来满足需求:当前实际读取的 size 是否小于或等于打算缩小的 sizeif (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {// 连续两次减小都可以if (decreaseNow) {// 减小index = max(index - INDEX_DECREMENT, minIndex);nextReceiveBufferSize = SIZE_TABLE[index];decreaseNow = false;} else {decreaseNow = true;}// 判断是否实际读取的数量大于等于预估的,如果是则尝试扩容} else if (actualReadBytes >= nextReceiveBufferSize) {index = min(index + INDEX_INCREMENT, maxIndex);nextReceiveBufferSize = SIZE_TABLE[index];decreaseNow = false;}}
3、零拷贝 - ( Zero-copy )
使用逻辑组合,代替复制
io.netty.buffer.CompositeByteBuf#addComponent使用包装,代替实际复制
byte[] bytes = data.getBytes();ByteBuf bytebuf = Unpooled.wrappedBuffer(bytes);
使用 JDK 的 Zero-Copy 接口io.netty.channel.DefaultFileRegion#transferTo
public long transferTo(WritableByteChannel target, long position) throws IOException {long count = this.count - position;if (count < 0 || position < 0) {throw new IllegalArgumentException("position out of range: " + position +" (expected: 0 - " + (this.count - 1) + ')');}if (count == 0) {return 0L;}if (refCnt() == 0) {throw new IllegalReferenceCountException(0);}// Call open to make sure fc is initialized. This is a no-oop if we called it before.open();// 包装 FileChannel.transferTo 方法 Zero-Copylong written = file.transferTo(this.position + position, count, target);if (written > 0) {transferred += written;} else if (written == 0) {// If the amount of written data is 0 we need to check if the requested count is bigger then the// actual file itself as it may have been truncated on disk.//// See https://github.com/netty/netty/issues/8868validate(this, position);}return written;}
4、堆外内存
堆内内存,把内存对象分配在 Java 虚拟机的堆以外的内存,又称直接内存。
5、内存池
内存池就是在程序启动时,预先向堆中申请一部分内存,交给一个管理对象。在程序运行中,需要时向管理对象“借”,不需要时“还”给管理对象。
常用开源实现 Apache Commons pool
Netty 轻量级内存池
io.netty.util.Recycler
四、Netty 内存使用源码分析
1、堆外内存
堆内内存 / 堆外内存的切换方式
指定参数:
io.netty.noPreferDirect = true / false默认不使用堆内内存的,可以这样指定使用堆内内存
ServerBootstrap b = new ServerBootstrap();b.childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator(false))
Netty 分配堆外内存的本质是调用 JDK 的
ByteBuffer.allocateDirect(initialCapacity);方法,再往下就是 JDK 的 Unsafe 了。
2、内存池
内存池 / 非内存池 的切换方式
指定参数 :
io.netty.allocator.type = unpooled / pooled启动类中指定配置:
ServerBootstrap b = new ServerBootstrap();b.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);
或
ServerBootstrap b = new ServerBootstrap();b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
那么 Netty 默认使用什么类型呢?
我们查看 io.netty.channel.DefaultChannelConfig 类:
private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
继续看 io.netty.buffer.ByteBufAllocator :
ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
继续看 io.netty.buffer.ByteBufUtil :
static final ByteBufAllocator DEFAULT_ALLOCATOR;static {// 系统变量中取值,若为安卓平台则使用 unpooledString allocType = SystemPropertyUtil.get("io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");allocType = allocType.toLowerCase(Locale.US).trim();ByteBufAllocator alloc;if ("unpooled".equals(allocType)) {alloc = UnpooledByteBufAllocator.DEFAULT;logger.debug("-Dio.netty.allocator.type: {}", allocType);} else if ("pooled".equals(allocType)) {alloc = PooledByteBufAllocator.DEFAULT;logger.debug("-Dio.netty.allocator.type: {}", allocType);} else {// 默认为内存池alloc = PooledByteBufAllocator.DEFAULT;logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);}DEFAULT_ALLOCATOR = alloc;THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);}
总结:默认情况下,安卓平台使用非池实现,其他平台使用内存池实现,在未指定
netty.allocator.type参数时,默认内存池实现。具体的内存池实现
io.netty.buffer.PooledDirectByteBuf我们看一下
PooledDirectByteBuf#newInstance方法:
private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(new ObjectCreator<PooledDirectByteBuf>() {public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {return new PooledDirectByteBuf(handle, 0);}});static PooledDirectByteBuf newInstance(int maxCapacity) {// 从池中获取PooledDirectByteBuf buf = RECYCLER.get();buf.reuse(maxCapacity);return buf;}
这个 RECYCLER 就是 Netty 的 Recycler 实现,
public final T get() {if (maxCapacityPerThread == 0) {// 表示没有开启池化配置,new Object 返回return newObject((Handle<T>) NOOP_HANDLE);}// ThreadLocal 获取返回Stack<T> stack = threadLocal.get();DefaultHandle<T> handle = stack.pop();if (handle == null) {// 池中没有对象时新建handle = stack.newHandle();handle.value = newObject(handle);}return (T) handle.value;}
上面的 get 方法时借,所谓有借有还再借不难,再看一下归还的方法(Recycler 的内部类 DefaultHandle ):
@Overridepublic void recycle(Object object) {if (object != value) {throw new IllegalArgumentException("object does not belong to handle");}Stack<?> stack = this.stack;if (lastRecycledId != recycleId || stack == null) {throw new IllegalStateException("recycled already");}// 归还回内存池stack.push(this);}
作者:wangning1018
地址:https://aysaml.com/articles/2020/11/03/1604391401499.html
- END -
▐往期推荐
如果你觉得文章不错,文末的赞 👍 又回来啦,记得给我「点赞」和「在看」哦~
