【深入理解 ByteBuf 之一】 release() 的必要性
引言
开个新坑 【深入理解 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 为什么没有相关容错的机制?
这个问题勾起了我的好奇心,而故事可能要从对象池的设计讲起
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!