news 2026/10/4 5:30:57

Spring Boot + Lettuce 堆外内存溢出:OutOfDirectMemoryError 复现与分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot + Lettuce 堆外内存溢出:OutOfDirectMemoryError 复现与分析

一、问题现象

异常信息

io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 2096896 byte(s) of direct memory (used: 7406854, max: 8388608)

业务影响

  • Redis 操作全部失败
  • 接口返回 500
  • 应用可能崩溃

二、问题本质

Netty 向操作系统申请的堆外内存超过了-XX:MaxDirectMemorySize的限制。

关键认知:

  • -Xmx管的是堆内存,管不了堆外内存
  • 堆外内存由-XX:MaxDirectMemorySize独立控制
  • Netty 的ByteBuf默认分配在堆外

三、复现环境

组件

版本

Spring Boot

2.2.5.RELEASE

JDK

8

Lettuce

5.2.2

Netty

4.1.45

JVM 参数

-Xms256m -Xmx256m -XX:MaxDirectMemorySize=8m

JMeter压测配置

  • 接口:/redis/big(大量数据set/get)
  • 线程数:200
  • 循环次数:不限制

四、复现步骤

  1. 建 Spring Boot 2.2.5 项目,引入spring-boot-starter-data-redis
  2. 关闭连接共享(share-native-connection: false)
  3. 写一个循环操作 Redis 的接口
  4. 启动参数加-XX:MaxDirectMemorySize=8m
  5. JMeter 200 并发压测

五、异常堆栈关键路径

io.netty.util.internal.OutOfDirectMemoryError at io.netty.util.internal.PlatformDependent.incrementMemoryCounter at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.reallocateDirect at io.netty.buffer.AbstractByteBuf.ensureWritable at io.lettuce.core.protocol.CommandArgs$ByteBufferArgument.writeByteBuf at io.lettuce.core.protocol.CommandEncoder.encode at io.netty.handler.codec.MessageToByteEncoder.write

关键点:Lettuce 在编码 Redis 命令时,通过 Netty 向堆外申请 ByteBuf,超过上限崩溃。

六、原因分析

6.1 完整因果链

高并发请求 → 每个请求操作 Redis → Lettuce 编码命令(编码结果是一个 ByteBuf) → ByteBuf 分配在堆外内存 → 所有命令进入同一个 EventLoop 队列(共享连接模式) → 队列无长度上限,堆积不断增长 → 每条排队命令都持有一份堆外 ByteBuf → 堆外内存占用持续增长 → 超过 MaxDirectMemorySize → 抛 OutOfDirectMemoryError

6.2 三个关键事实

事实一:-Xmx管不住堆外内存

堆内存

堆外内存

上限参数

-Xmx

-XX:MaxDirectMemorySize

回收机制

Young GC / Full GC

Cleaner 或 Full GC 触发

溢出异常

Java heap space

Direct buffer memory/OutOfDirectMemoryError

是否独立

独立

独立

事实二:Lettuce 默认共享连接,同一个 EventLoop 队列

Lettuce 底层用 Netty 的Channel。Netty 的Channel是线程安全的:

  • 多个线程可以并发调用channel.write(command)
  • Netty 内部把这些写操作封装成任务,投递到 Channel 绑定的EventLoop 线程的队列里
  • EventLoop单线程串行执行队列里的任务,最终按顺序写入 socket

所以默认情况下,所有线程共用同一个 SocketChannel(同一条 TCP 连接)。

事实三:排队的命令都占着堆外 ByteBuf

每条 Redis 命令在写入 socket 之前,都要先编码成ByteBuf。这个ByteBuf分配在堆外。

命令在 EventLoop 队列里排队时,已经编码好的 ByteBuf 仍然占着堆外内存。队列越长,堆外占用越高。

6.3 为什么共享连接模式容易爆

100 线程 → 【无闸门】→ 1 个 EventLoop 队列 → 队列长度无上限 → 每条占一份堆外 ByteBuf → 堆外暴涨 → OOM

核心问题:入口没有并发闸门,队列没有长度限制,堆外占用与队列长度成正比,没有上限。

6.4 连接池如何解决(背压思想)

100 线程 → 【池大小=8 的闸门】→ 8 个连接 ↓ 借不到连接 → 线程在这里等待(不占堆外内存) ↓ 等待超时 → 抛异常 ↓ 借到连接 → 进入该连接的 EventLoop 队列 ↓ 堆外占用受 8 个连接上限约束

关键点:

  • 连接池限制的不是队列长度,而是活跃连接数
  • 没抢到连接的线程在连接池层面等待,不进入 Netty 队列
  • 等待只占一个线程,不占堆外内存
  • Netty 本身不做"等待"——分配失败时直接抛OutOfDirectMemoryError

本质:连接池用"等待"(软约束、可恢复)替换了"内存堆积"(硬约束、不可恢复)。这就是背压(Backpressure)。

6.5 三层内存保护模型

连接池只能管住入口层,完整的内存安全靠三层:

层级

措施

解决什么

入口层

连接池限制活跃连接数

防止并发把堆外占满

单连接层

命令队列长度、pipeline 批量大小

防止单连接内队列堆积

单操作层

限制 value 大小

防止单个命令本身就超上限

连接池解决的是并发峰值堆积,不是绝对内存不溢出。如果单个操作需要的内存超过整个堆外上限(如MaxDirectMemorySize=8m却要写 10MB 的 value),任何框架都救不了,只能调大上限或拆小数据。

七、版本差异

版本组合

能否复现

原因

Spring Boot 2.2.5 + Lettuce 5.2.2

✅ 能

无连接池 + 旧版 Netty 分配策略

Spring Boot 2.7.0 + Lettuce 6.1.8

❌ 不能

连接池 + 新版 Netty 优化

7.1 新版为什么不会炸

变化一:Lettuce 6.x 默认启用连接池,形成并发闸门

  • 连接数有上限(默认 8)
  • 共享连接模式被削弱,等待发生在连接池层
  • 堆外占用 = 连接数 × 单连接缓冲,总量受限

变化二:Netty 池化分配器更成熟,缓冲区可复用

  • PooledByteBufAllocator从本地池取 ByteBuf,用完归还
  • 不需要频繁向操作系统申请/释放堆外内存
  • 单连接的堆外占用稳定,不随并发线性增长

7.2 深层对比

维度

旧版(会炸)

新版(不会炸)

并发闸门

无(共享连接)

连接池上限

队列堆积位置

Netty 队列(占堆外)

连接池等待队列(占线程)

堆外占用上限

无上限

连接数 × 单连接缓冲

Netty 分配策略

较激进

池化复用,更克制

后果

OOM(不可恢复)

超时(可重试/降级)

八、解决方案

方案

说明

升级 Spring Boot

2.7+ 默认 Lettuce 6.x,问题自动消失

调大堆外内存

-XX:MaxDirectMemorySize=256m或更大

换 Jedis

Jedis 用连接池,不走 Netty 堆外

限制并发

降低连接数,减少堆外申请

补充说明:调大MaxDirectMemorySize是治标不治本,因为堆外占用如果与并发线性相关,迟早会再爆。首选升级版本,其次是引入连接池,最后才是调大上限。

九、排查工具

  • NMT:jcmd <pid> VM.native_memory summary看堆外使用
  • 启动参数:-XX:NativeMemoryTracking=summary
  • pmap:pmap -x <pid>看进程内存分布

十、仓库地址

https://gitee.com/shun-chun-li/springboot-lettuce-direct-memory-oom

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 5:29:53

基于SpringBoot+Vue的护理知识学习咨询系统设计与实现

选题背景 随着信息技术的迅猛发展与医疗健康领域数字化转型的不断深入&#xff0c;传统护理教育模式正面临前所未有的挑战与变革。护理工作作为医疗体系中的重要组成部分&#xff0c;其专业性、实践性和持续学习需求尤为突出。然而&#xff0c;当前许多医疗机构和护理院校在护理…

作者头像 李华
网站建设 2026/10/4 5:28:07

LabVIEW多通道采集实战:热电偶与TTL转速信号同步方案

做发动机测试台架的时候&#xff0c;遇到最多的一件事就是用LabVIEW配合DAQ设备采集一堆传感器信号。温度要采&#xff0c;转速要采&#xff0c;有时候还得同时采好几个通道&#xff0c;模拟量和数字量混在一起。前阵子刚好把一套多通道采集程序从头到尾捋了一遍&#xff0c;采…

作者头像 李华
网站建设 2026/10/4 5:27:51

TCP/UDP调试工具实战:从连接建立到协议验证的完整指南

简介&#xff1a;TCP&UDPDebug 是一款面向网络编程开发者与运维调试人员的传输层协议测试工具&#xff0c;用于在开发 TCP 服务器、客户端或 UDP 服务时验证连接性能、排查通信异常。工具围绕连接建立与断开、数据收发、丢包检测、顺序校验、错误分析、吞吐量与延迟监控、端…

作者头像 李华
网站建设 2026/10/4 5:23:06

OpenCV原生meshgrid:零拷贝高性能坐标网格生成术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 5:23:06

TIL 实战:用 aws ecs execute-command 三步进入 ECS 容器

文档教程知识库 【免费下载链接】til :memo: Today I Learned 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ti/til 点击查看 免费下载 生产环境里的 Rails&#xff08;或其他 Web&#xff09;应用跑在 AWS ECS 容器中时&#xff0c;经常需要临时打开一个交互式 shel…

作者头像 李华
网站建设 2026/10/4 5:20:51

AI写网页总跑偏?一套需求模板让代码一次生成可用

1. 为什么 AI 写代码总是"跑偏"1.1 一个几乎所有人都踩过的坑你打开 AI 对话窗口&#xff0c;敲下"帮我写一个网页"&#xff0c;回车。几秒钟后&#xff0c;屏幕上刷出一大段 HTML、CSS、JavaScript 混在一起的代码。你满怀期待地复制到一个.html文件里&am…

作者头像 李华