首页 > 系统相关 >生产环境Java应用服务内存泄漏分析与解决

生产环境Java应用服务内存泄漏分析与解决

时间:2023-03-09 23:45:11浏览次数:47  
标签:ehcache bufferCache Java XX 缓存 内存 应用服务 DirectByteBuffer

有个生产环境CRM业务应用服务,情况有些奇怪,监控数据显示内存异常。内存使用率99.%多。通过生产监控看板发现,CRM内存超配或内存泄漏的现象,下面分析一下这个问题过程记录。

服务器配置情况:

生产服务器采用阿里云ECS机器,配置是4HZ、8GB,单个应用服务独占,CRM应用独立部署,即单台服务器仅部署一个java应用服务。

用了4个节点4台机器,每台机器都差不多情况。

监控看板如下:

 

内存分布统计:

从监控看板的数据来看,我们简单统计一下内存分配数据情况。

应用启动配置参数:

 /usr/bin/java

-javaagent:/home/agent/skywalking-agent.jar

-Dskywalking.agent.service_name=xx-crm

 -XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/xx-crm.hprof

-Dspring.profiles.active=prod

-server -Xms4884m -Xmx4884m -Xmn3584m

-XX:MetaspaceSize=512m

-XX:MaxMetaspaceSize=512m

-XX:CompressedClassSpaceSize=128m

-jar /home/xxs-crm.jar

 

 

堆内存 4.8G左右,其中新生代3.5G左右,

非堆内存:(Metaspace)512M+(CompressedClassSpace)128M+(Code Cache)240M约等1G左右.

堆内存(heap)+非堆内存(non-Heap)=5.8G,8GB物理内存除去操作系统本身占用大概500M,起码至少还有1~2GB空闲才合理呀!怎么竟然占了99%多,就意味着有1~2G不知道谁占去了,有点诡异!

 

先看一下JVM内存模型,环境是使用JDK8

JVM内存数据分区:

 

堆heap结构:

堆大家都比较容易理解的,也是java程序接触得最多的一块,不存在什么数据上统计错误,或占用不算之类的。

那说明额外占用也非堆里面,只不过没有统计到非堆里面去,曾经一度怀疑监控prometheus展示的数据有误

先看一下dump文件数据,这里使用MAT工具(一个开源免费的内存分析工具,个人认为比较好用,推荐大家使用)。

 

通过下载内存dump镜像观察到

 

有个offHeapStore,这个东西堆外内存,可以初步判断是 ehcahe引起的。

通过ehcahe源码分析,发现ehcache里面也使用了netty的NIO方法内存,ehcache磁盘缓存写数据时会用到DirectByteBuffer。

DirectByteBuffer是使用非堆内存,不受GC影响。

 

当有文件需要暂存到ehcache的磁盘缓存时,使用到了NIO中的FileChannel来读取文件,默认ehcache使用了堆内的HeapByteBuffer来给FileChannel作为读取文件的缓冲,FileChannel读取文件使用的IOUtil的read方法,针对HeapByteBuffer底层还用到一个临时的DirectByteBuffer来和操作系统进行直接的交互。

 

ehcache使用HeapByteBuffer作为读文件缓冲:

 

IOUtil对于HeapByteBuffer实际会用到一个临时的DirectByteBuffer来和操作系统进行交互。

 

 

DirectByteBuffer泄漏根因分析

默认情况下这个临时的DirectByteBuffer会被缓存在一个ThreadLocal的bufferCache里不会释放,每一个bufferCache有一个DirectByteBuffer的数组,每次当前线程需要使用到临时DirectByteBuffer时会取出自己bufferCache里的DirectByteBuffer数据,选取一个不小于所需size的,如果bufferCache为空或者没有符合的,就会调用Bits重新创建一个,使用完之后再缓存到bufferCache里。

这里的问题在于 :这个bufferCache是ThreadLocal的,意味着极端情况下有N个调用线程就会有N组 bufferCache,就会有N组DirectByteBuffer被缓存起来不被释放,而且不同于在IO时直接使用DirectByteBuffer,这N组DirectByteBuffer连GC时都不会回收。我们的文件服务在读写ehcache的磁盘缓存时直接使用的tomcat的worker线程池,

这个worker线程池的配置上限是2000,我们的配置中心上的配置的参数:

 

所以,这种隐藏的问题影响所有使用到HeapByteBuffer的地方而且很隐秘,由于在CRM服务中大量使用了ehcache存在较大的sizeIO且调用线程比较多的场景下容易暴露出来。

 

获取临时DirectByteBuffer的逻辑:


bufferCache从ByteBuffer数组里选取合适的ByteBuffer:

 

将ByteBuffer回种到bufferCache:

 

NIO中的FileChannelSocketChannelChannel默认在通过IOUtil进行IO读写操作时,除了会使用HeapByteBuffer作为和应用程序的对接缓冲,但在底层还会使用一个临时的DirectByteBuffer来和系统进行真正的IO交互,为提高性能,当使用完后这个临时的DirectByteBuffer会被存放到ThreadLocal的缓存中不会释放,当直接使用HeapByteBuffer的线程数较多或者IO操作的size较大时,会导致这些临时的DirectByteBuffer占用大量堆外直接内存造成泄漏。

那么除了减少直接调用ehcache读写的线程数有没有其他办法能解决这个问题?并发比较高的场景下意味着减少业务线程数不是一个好办法。

在Java1.8_102版本开始,官方提供一个参数jdk.nio.maxCachedBufferSize,这个参数用于限制可以被缓存的DirectByteBuffer的大小,对于超过这个限制的DirectByteBuffer不会被缓存到ThreadLocal的bufferCache中,这样就能被GC正常回收掉。唯一的缺点是读写的性能会稍差一些,毕竟创建一个新的DirectByteBuffer的代价也不小,当然如上面列出的,性能也没有数量级的差别。

增加参数:

-XX:MaxDirectMemorySize=1600m
-Djdk.nio.maxCachedBufferSize=500000    ---注意不能带单位

就是调整了-Djdk.nio.maxCachedBufferSize=500000(注意这里是字节数,不能用mkg等单位)。

增加调整参数之后,运行一段时间,持续观察整体DirectByteBuffer稳定控制在1.5G左右,性能也几乎没有衰减。一切恢复正常,再看监控看板没有看到占满内存告警。

  

业务系统调整后的启动命令参数如下:

 java

-javaagent:/home/agent/skywalking-agent.jar

-Dskywalking.agent.service_name=xx-crm

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/xx-crm.hprof

-Dspring.profiles.active=prod

-server -Xms4608m -Xmx4608m -Xmn3072m

-XX:MetaspaceSize=300m

-XX:MaxMetaspaceSize=512m

-XX:CompressedClassSpaceSize=64m

-XX:MaxDirectMemorySize=1600m

-Djdk.nio.maxCachedBufferSize=500000

-jar /home/xx-crm.jar

 

 

参考文章《Troubleshooting Problems With Native (Off-Heap) Memory in Java Applications》:

https://dzone.com/articles/troubleshooting-problems-with-native-off-heap-memo

 

标签:ehcache,bufferCache,Java,XX,缓存,内存,应用服务,DirectByteBuffer
From: https://www.cnblogs.com/cgli/p/17201943.html

相关文章

  • 关于JAVA泛型数组类型擦除引发的问题及解决方案
    先看如下一个DEMO示例代码:(其中doBatchGet被子类重写了1次)publicabstractclassBaseDemoService<T>{publicStringbatchGet(T...ints){Tone=ints[......
  • 61.动态内存和类
    程序清单12.1strngbad.h#pragmaonce//strngbad.h--有缺陷的string类定义#include<iostream>#ifndefSTRNGBAD_H_#defineSTRNGBAD_H_classStringBad{private......
  • java中的泛型
    @目录1泛型概述2泛型格式3泛型增强3.1泛型方法单一方法增强整体方法增强3.2泛型类3.3泛型接口约束模式1泛型概述参数化类型。在不创建新的类型的情况下,通过泛型指定的不......
  • Java基础——HashMap 的长度为什么是 2 的幂次方
    HashMap的长度为什么是2的幂次方为了能让HashMap存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash值的范围值-2147483648到2147483647......
  • java8新特性/函数式编程/lamda/stream流
    新特性简介   java8内置的四大核心函数式接口          其他接口  方法引用               构造使用......
  • Java实现对象空属性(空字符串)转null
    @Slf4jpublicclassConvertUtils{/***@Description主要解决查询时前端传参为空值("")*BeanUtils.copyProperties会把空值带入目标对象中*......
  • Java数据类型转换
    类型转换由于Java是强类型语言,所以要进行有些运算的时候需要用到类型转换。低 ---------------------------------> 高byte,short,char->int->long->float->doub......
  • Java Set Summary
    JavaSetSummary一、概要Set6个类名since线程安全elementnull特点Set1.2HashSet1.2NoYes基于HashMap实现TreeSet1.2NoNo基于TreeMa......
  • [java-project-gl]接口幂等性
    接口幂等性一、什么是幂等性接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支......
  • [java-project-gl]分布式缓存
    分布式缓存缓存常见的问题缓存穿透缓存和数据库中都没有的数据,而用户不断发起请求,导致数据压力过大,甚至击垮数据库比如黑客会对你的系统进行攻击,拿一个不存在的id去查......