【深入理解 ByteBuf 之一】 release() 的必要性

2024-01-08 02:11:14

引言

开个新坑 【深入理解 ByteBuf】 至于为什么,本篇就是原因
我大概会花一个较长的时间来剖析 Netty 对于 ByteBuf 的实现,对象池的设计,从分配到释放重用,希望可以借此学习理解对象池的设计思想,以及搞清楚,我们不 release ByteBuf 泄露的究竟是什么,
以文章形式作为记录,希望对之后的开发设计有所启发,如果本系列文章帮助到了你,不胜荣幸。

ByteBuf 不 release 造成的内存泄露

首先模拟内存泄露的场景,这里我写了几个接口先通过 /buffer 接口进行循环分配,/metric 接口可以查看当前 分配器 的一些状态参数

@Slf4j
@RequestMapping("/api/bytebuf")
@RestController
public class ByteBufTestController {

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,
            10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));

    @RequestMapping("/bufferNoPool")
    public ResultVO bufferNoPool() {


        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        final ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(1024 * 10);
                        buffer.writeBytes(new byte[1024 * 10]);
                        buffer.retain();
                    }
                }catch (Exception e){
                    log.error("run buffer error" ,e);
                }

            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        return ResultVO.successResult(PooledByteBufAllocator.DEFAULT.toString());
    }

    @RequestMapping("/buffer")
    public ResultVO buffer() {


        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        final ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(1024 * 10);
                        buffer.writeBytes(new byte[1024 * 10]);
                        buffer.retain();
                    }
                }catch (Exception e){
                    log.error("run buffer error" ,e);
                }

            }
        };
        threadPoolExecutor.submit(runnable);
        threadPoolExecutor.submit(runnable);
        return ResultVO.successResult(PooledByteBufAllocator.DEFAULT.toString());
    }

    @RequestMapping("/bufferAndRelease")
    public ResultVO bufferAndRelease() {


        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        final ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(1024 * 10);
                        buffer.writeBytes(new byte[1024 * 10]);
                        buffer.release();
                    }
                }catch (Exception e){
                    log.error("run bufferAndRelease error" ,e);
                }

            }
        };
        threadPoolExecutor.submit(runnable);
        threadPoolExecutor.submit(runnable);
        return ResultVO.successResult(PooledByteBufAllocator.DEFAULT.toString());
    }

    @RequestMapping("/metric")
    public ResultVO metric() {
        final PooledByteBufAllocatorMetric metric = PooledByteBufAllocator.DEFAULT.metric();
        ResultMap resultMap = new ResultMap();
        resultMap.put("numDirectArenas", metric.numDirectArenas());
        resultMap.put("usedDirectMemory", metric.usedDirectMemory());
        resultMap.put("metric", metric.toString());
        return ResultVO.successResult(resultMap);
    }

}

配置好你本地的启动项,为了观测明显我分配了 2G 的可用直接内存

-Xms2G
-Xmx2G
-XX:MaxDirectMemorySize=2G
-XX:ThreadStackSize=512
-XX:MaxMetaspaceSize=256M
-XX:MetaspaceSize=256M
-Dio.netty.leakDetection.level=paranoid
--add-opens
java.base/java.lang=ALL-UNNAMED
--add-opens
java.base/java.io=ALL-UNNAMED
--add-opens
java.base/java.math=ALL-UNNAMED
--add-opens
java.base/java.net=ALL-UNNAMED
--add-opens
java.base/java.nio=ALL-UNNAMED
--add-opens
java.base/java.security=ALL-UNNAMED
--add-opens
java.base/java.text=ALL-UNNAMED
--add-opens
java.base/java.time=ALL-UNNAMED
--add-opens
java.base/java.util=ALL-UNNAMED
--add-opens
java.base/JDK.internal.access=ALL-UNNAMED
--add-opens
java.base/JDK.internal.misc=ALL-UNNAMED
--add-opens
java.base/sun.net.util=ALL-UNNAMED

在这里插入图片描述

那项目刚启动可以观察到,在活动监视器中的内存占用是 1G 多

在这里插入图片描述

运行时可以尝试通过 JProfiler 来监听内存和 GC 情况,下面是正常运行的检测

在这里插入图片描述

在这里插入图片描述

请求分配并且不释放时,堆内存增长,经过 GC 后呈现尖刺状,最后趋近平稳是线程分配已经报错,无法进行分配了,可以看到整个 Java 程序占用 4G 多而且一直不会释放。

在这里插入图片描述

再次 GC 后回归正常

在这里插入图片描述

但是整个程序堆外内存已经无法分配了

在这里插入图片描述

Exception in thread "Thread-53" java.lang.OutOfMemoryError: Cannot reserve 4194304 bytes of direct buffer memory (allocated: 2146073230, limit: 2147483648)
	at java.base/java.nio.Bits.reserveMemory(Bits.java:178)
	at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:121)
	at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:332)
	at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:701)
	at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:676)
	at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:215)
	at io.netty.buffer.PoolArena.tcacheAllocateSmall(PoolArena.java:180)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:137)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:129)
	at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:400)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:179)
	at io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:116)

可以看到这里报错的限制值与配置的 2G 是一致的

在这里插入图片描述

那其实能看到明显的堆内存浮动是因为我代码中分配 ByteBuf 的时候同时 new 了一个 byte 数组,去掉这行代码同样可以观察到堆外内存一直居高不下,堆内存没有影响,只有一次明显的 GC 活动

 buffer.writeBytes(new byte[1024 * 10]);

在这里插入图片描述

这说明如果你没有正确的 release ByteBuf 会导致堆外内存无法释放,从而导致内存泄露,再次尝试申请会报 OOM 错误。

也就是说即使 JVM 帮你回收了没有引用的 ByteBuf,但是 ByteBuf 占用的堆外内存也不会得到释放

at java.base/java.nio.Bits.reserveMemory(Bits.java:178)

在这里插入图片描述

如果调用的是分配并正确释放方法,可以观察到内存的使用是稳定的,GC 来自于堆内引用的申请和释放

在这里插入图片描述

至此已经复现了问题,并认识到了其严重性,那么具体到代码里,究竟是什么没有释放呢?Netty 为什么没有相关容错的机制?

这个问题勾起了我的好奇心,而故事可能要从对象池的设计讲起

文章来源:https://blog.csdn.net/w903328615/article/details/135414777
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。