前天同事在我们的数据服务spring boot项目中发现请求某个接口,导致内存不停增长,最后导致内存溢出,程序崩溃的问题。最初怀疑是不是有什么大对象在JVM中进行了垃圾回收也没有释放,顺着这个思路一步步排查。
- 使用jstat查看垃圾回收频率
jstat -gc 1 250 20
发现MinorGC频率很低,FullGC频率很高。初步定位是老年代内存分配太少引起的。过然查看java程序启动参数发现以下设定:"-Xms300m","-Xmx300m","-MaxNewSize=300"
很显然这个配置存在明显的问题,整个堆分配300m内存,给新生代的是300m,导致老年代几乎没有内存可以使用,所以会导致频繁的FullGC。把MaxNewSize去掉,使用系统默认的2:8。然后观察垃圾回收情况,这次不会频繁发生FullGC了,但是内存还是一直增长,直到内存溢出。
- 使用jmap查看堆对象
jmap -dump:format=b,file=test.bin pid
因为出现的问题在生产服务器中,使用JProfiler远程连接以前没有试过。暂时先用JVM自带的工具jmap把堆快照下载下来,然后使用本地JProfiler进行分析。初步定为发现LauchedURLClassLoader占用了很多内存,但是由于分析的是快照,没法知道该对象的增长速度,无果。
-
在本地使用JProfiler实时监控分析
使用JVM自带的工具能看到的信息有限。权衡使用JProfiler远程连接定位还不如在本地开启调试环境,然后使用JProfiler附加该java进程监控。标记内存分配比较多的大对象,然后模拟点击持续请求,发现很多对象的调用实例都一直在增加,然后观察线程一直在增加并且停止请求,线程也没有释放。线程名称I/O dispatcher,查看线程堆栈是一个NIO线程,由AbstractMultiworkerIOReactor创建。通过包名不难定位是引用的CloseableHttpAsyncClient异步http请求的问题。然后在代码端查找使用该类的地方,发现该段代码中使用ES进行搜索,而搜索用的对象RestHighLevelClient,内部使用的是异步http请求。至此问题已经定位到,是es使用的相关问题。经验证把es相关代码注释掉,程序运行正常,线程分配也正常。
- 调整RestHighLevelClient的使用方式
定位到是RestHighLevelClient使用的问题,开始怀疑是否是使用的姿势不对,于是查看文档,然后设置CloseableHttpAsyncClient的IO线程数等一系列参数,线程还是暴涨,无果。下面是在es github和论坛中查看的一些issues.
// Hight I/O dispatch Thread
https://github.com/elastic/elasticsearch/issues/61675
// Direct buffer memory problems with RestHighLevelClient
https://discuss.elastic.co/t/direct-buffer-memory-problems-with-resthighlevelclient/106647)
- 通过源码定位到问题
无奈只能硬着头皮查看源码(具体对CloseableHttpAsyncClient源码分析有时间再写一篇笔记),无意跟踪代码的过程中发现RestHighLevelClient每次请求的过程中,都会初始化,然后初始化资源也没有释放。
public RestHighLevelClient(RestClientBuilder restClientBuilder) {
this(restClientBuilder, Collections.emptyList());
}
继续跟踪本地自己写的代码,发现在一个Optional初始化的过程中在orElse有一个初始化的代码。经调试验证,原来orElse就算不走该分支,程序执行的过程中会预先初始化。在RestHighLevelClient预先分配了线程,然后程序也没有释放,所以导致线程溢出和内存溢出,去掉orElse,然后重新测试观察JProfiler中线程的趋势图已经正常,至此问题解决。