从 `ClientboundCustomPayloadPacket.getInternalData()` 中汇总的直接 ByteBuf 拷贝在修改后的负载路径上永远不会被释放(每个包的直接内存泄漏;高流量包时客户端端出现内存溢出)

作者: ecpunk创建于 2026年7月22日更新于 2026年7月24日
标签Triage

**Minecraft 版本:** 1.20.1 **Forge 版本:** 47.4.18 (在其中重现;相关的 `net.minecraftforge.network` 代码在当前的 47.4.22 中没有变化,因此也存在于其中)。附带的 Netty 4.1.82。 **日志:** 附件:`leak_records_excerpt.txt` — 来自 61 分钟的会话中,有 3 个完整的 netty LEAK 记录,共产生了 7,282 个,所有记录共享一个创建时间堆栈。完整的泄漏检测会话日志、Eclipse MAT 报告和堆栈转储可按要求获取。 **重现步骤:** 1. 客户端安装了一个或多个使用 `SimpleChannel` 的模块,连接到运行相同模块的专用服务器(任何定期同步的模块都可以;更多/聊天的模块只会使其更快)。 2. 将 `-Dio.netty.leakDetection.level=advanced` 添加到客户端 JVM 参数中,或者监视 `java.nio:type=BufferPool,name=direct` MBean(例如通过 JConsole)。 3. 玩游戏或只是静待。直接池按接收自定义负载流量的比例单调增长,永远不会返回;在启用泄漏检测时,`LEAK:` 记录将显示下面的创建时间堆栈。 4. 要确认它是被锁定而不是被回收:运行 `jcmd <pid> GC.run` — 池不会缩小。一旦 `-XX:MaxDirectMemorySize` 耗尽,客户端将被踢出,并抛出 `OutOfMemoryError: Cannot reserve N bytes of direct buffer memory`。 **问题描述:** 在客户端上,每个通过 Forge 的 `SimpleChannel` 处理的自定义负载包都会泄露一个池化的 **直接** `ByteBuf`。 Forge 会对每个自定义负载(`ClientboundCustomPayloadPacket.getInternalData()` → `new FriendlyByteBuf(f_132030_.copy())`)进行防御性拷贝,将其存储在 `NetworkEvent.payload` 中,将其发送给注册的处理程序,然后返回,而不调用 `release()` 对该拷贝。池化的直接内存只能通过 `release()` 回收,而不能通过 GC 回收,因此在会话期间池会单调增长。在一个高度修改的包(数百个模块,高 `SimpleChannel` 同步包量)中,我们测量了每个客户端约 **70 MB/分钟的直接内存增长**。纯净版本不受影响(没有修改的负载采用此路径);轻度修改的设置泄漏速度太慢以至于无法注意到,但遗漏对任何 `SimpleChannel` 流量都是存在的。 我们有一个客户端侧修复方案和完整的泄漏检测器以及堆栈转储证据,我们很乐意开一个 PR。我们知道 1.20.1 处于维护阶段,可能只会采取关键性修复 — 此问题主要作为数据和修复方案提交,而不是优先级请求。 ### 为什么基本上每个 SimpleChannel 模块都会重现问题 该泄漏与模块无关。任何注册了 `SimpleChannel` 的模块都会产生此问题。我们已经在客户端侧修复了此问题,并提供了完整的泄漏检测器和堆栈转储证据,我们很乐意开一个 PR。我们理解 1.20.1 处于维护阶段,可能只会采取关键性修复 — 此问题主要作为数据和修复方案提交,而不是优先级请求。

内容来源: MinecraftForge/MinecraftForge