1.问题描述
Java后端服务接口报堆外内存溢出错误。
2.问题报错
ERROR outOfDirectMemoryError:failed to allocate 134217728 byte(s) of direct memory (used:146800640, max:279969792)
3.问题报错日志截图
4.问题分析
可能导致堆外内存溢出的原因:
- 系统所在服务器物理内存不足。
- JVM参数设置不正确。
- 接口方法编程错误,如ByteBuffer可能在使用完毕后未被正确释放。
- 使用第三方库(如redis客户端等)存在内存管理问题。
拓展分析:
Java 中的内存溢出错误通常指的是 OutOfMemoryError,它表明 Java 虚拟机(JVM)在尝试为对象分配内存时,内存不足,且垃圾回收器也无法回收足够的空间来满足请求。OutOfMemoryError 有多种,包括但不限于:
- 堆内存溢出:当 Java 堆(Heap)没有足够的空间来创建新的对象时,会抛出 OutOfMemoryError: Java heap space。这通常是因为应用程序创建了太多对象,或者长时间运行后累积了太多无法被垃圾回收器回收的对象。
2、永久代/元空间溢出:在 Java 8 之前,OutOfMemoryError: PermGen space 错误表明永久代(PermGen)内存不足。永久代用于存储类的元数据。从 Java 8 开始,永久代被元空间(Metaspace)取代,如果元空间不足,会抛出 OutOfMemoryError: Metaspace。
3、直接内存溢出:直接内存(Direct Memory)是 JVM 之外的内存,由 java.nio.ByteBuffer 的某些实现(如 DirectByteBuffer)使用。如果直接内存不足,会抛出 OutOfMemoryError: Direct buffer memory。
5.排查步骤
1、使用free命令检查系统内存
# -h 选项表示以人类可读的格式(如G、M)显示
free -h
经排查,系统有足够的物理内存来支持应用程序运行。
2、使用内存诊断工具(如 jconsole、jvisualvm 或 jmap)查找问题
通过jvisualvm查看应用程序的GC的情况。
如上图所示:伊甸园区满了触发Minor GC(年轻代垃圾回收),老年代也没有做完全的移出,包括我们的元空间也一直没有占用多少,说明我们的GC是正常的,没有什么问题。
拓展说明:
Java虚拟机(JVM)的内存管理中新生代内存进一步划分为伊甸园区(Eden Space)、幸存者0区(Survivor 0 Space)和幸存者1区(Survivor 1 Space),其中所有Java对象都在伊甸园区中出生,当伊甸园区内存空间不足时,会触发Minor GC(年轻代垃圾回收),将存活的对象移动到幸存者区(Survivor Space)。
3、检查 JVM 参数设置
将-Xmx参数从100m调整为300m,接口访问恢复正常,但在大并发的情况下还是会报堆外内存溢出,问题仍未解决。
4、排查java后端接口代码问题
经排查,接口方法本身也没有问题。
5、检查依赖库及其他资源内存使用情况
经排查,项目中使用redis作为缓存中间件,springboot2.0以后默认使用lettuce作为操作redis的客户端,通过查看lettcue-core源码发现,它使用netty进行网络通信,lettuce的bug导致netty堆外内存溢出。
诊断结果:
项目中springboot2.0默认使用的 redis的客户端lettuce-core的bug导致netty堆外内存溢出。netty的特点是:如果没有为其指定堆外内存,默认使用Java虚拟机的内存,内存没有得到及时释放。
6.解决方法
6.1更换项目中使用的redis客户端
项目中放弃使用lettcue,在项目中使用jedis作为连接redis的客户端,在Spring Boot中,移除默认的Lettuce作为Redis客户端,并改用Jedis,通常不需要显式地从依赖中移除Lettuce,因为Spring Boot的spring-boot-starter-data-redis并不直接包含Lettuce的依赖,而是依赖于底层可能包含Lettuce的库。但是,你需要通过配置来确保Spring Boot使用Jedis而不是Lettuce。具体操作步骤如下:
1、确保项目中没有直接包含Lettuce的依赖。
检查pom.xml(Maven)文件,确保没有直接包含Lettuce的依赖。通常spring-boot-starter-data-redis已经足够,它会根据你的配置或类路径中的依赖自动选择使用Lettuce还是Jedis。
拓展说明:如果你的项目用的是gradle则检查build.gradle(Gradle)文件。
2、添加Jedis的依赖。
通常spring-boot-starter-data-redis已经包含了Jedis的依赖,这个步骤不是必要的,但为了确保使用了Jedis并为其指定一个特定的版本,你在pom.xml需添加以下依赖:
Maven:
<dependency> | |
<groupId>redis.clients</groupId> | |
<artifactId>jedis</artifactId> | |
<version>3.0.0</version> <!-- 替换为实际的版本号 --> | |
</dependency> |
拓展说明:如果你的项目用的是gradle则添加如下依赖。
Gradle:
dependencies { | |
implementation 'redis.clients:jedis:3.0.0' // 替换为实际的版本号 | |
} |
3、配置Spring Boot使用Jedis。
在application.properties或application.yml文件中,不需要显式地指定使用Jedis,因为Spring Boot会根据类路径中的依赖自动选择,直接配置Jedis连接池的属性即可。
application.properties:
spring.redis.jedis.pool.max-active=8 | |
spring.redis.jedis.pool.max-wait=-1ms | |
spring.redis.jedis.pool.max-idle=8 | |
spring.redis.jedis.pool.min-idle=0 | |
spring.redis.client-type=jedis |
application.yml:
spring: | |
redis: | |
jedis: | |
pool: | |
max-active: 8 | |
max-wait: -1ms | |
max-idle: 8 | |
min-idle: 0 | |
client-type: jedis |
注意spring.redis.client-type=jedis这个属性明确告诉Spring Boot使用Jedis作为Redis客户端。这个属性在较新版本的Spring Boot中不是必需的,因为Spring Boot通常可以自动检测并使用类路径中的Jedis。
4、清理和重建项目
执行Maven的clean install命令来清理并重建项目。
拓展说明:如果你的项目用的是gradle则使用clean build命令来清理并重建项目。
5、验证
高并发访问接口验证内存溢出问题,已修复。
拓展说明:
我们也可以不更换redis客户端去采用升级lettuce版本的方式解决该问题,但是lettuce目前没有一个版本可以解决堆外内存溢出的问题,因此我们不能直接使用官方版本升级方式去解决这个问题,我们可以自己改源码去解决这个问题。该方法解决起来比较耗时费力,未使用该方法,解决思路如下,以供参考:ByteBuffer是Java NIO中一个非常重要的组件,在网络编程中非常有用,你可以尝试重用 ByteBuffer 对象而不是每次都创建新的。
6.2调整JVM参数
使用 -XX:MaxDirectMemorySize JVM 参数来设置允许的最大直接内存大小,确保这个值不要超过系统的可用物理内存。我们可以通过命令行和设置tomcat配置文件中JVM参数两种方式进行调整JVM参数:
1、在命令行中:当启动Java应用程序时,将参数添加到java命令中。例如:
java -XX:MaxDirectMemorySize=1024m -jar your-application.jar |
在这个例子中,我们设置了直接内存的最大大小为1024MB。
2、设置tomcat配置文件中JVM参数
(1)打开Tomcat的bin目录。
(2)配置catalina文件并JAVA_OPTS参数
Windows
找到catalina.bat文件(用于启动Tomcat的批处理文件)。
在catalina.bat文件中,找到类似于set JAVA_OPTS=的行(如果不存在,则添加它)。
在这行后面添加你的JVM参数:
set JAVA_OPTS=-XX:MaxDirectMemorySize=1024m |
Linux/macOS
找到catalina.sh文件(用于启动Tomcat的shell脚本)。
在catalina.sh文件中,找到类似于export JAVA_OPTS=的行(如果不存在,则添加它)。
在这行后面添加你的JVM参数,例如:
export JAVA_OPTS="$JAVA_OPTS -XX:MaxDirectMemorySize=1024m" |
注意:若已经设置了JAVA_OPTS,那么只需在现有值后面添加参数即可,确保用空格分隔。
保存并关闭文件。该步骤请根据实际情况按需配置。
(3)重新启动Tomcat以应用更改。
6.3增加服务器物理内存
云服务器直接弹性扩容服务器物理内存。如果是物理服务器,则通过加配内存条方式扩容。
说明:该步骤请根据实际情况按需配置。