介绍
背景介绍
最近公司要上线一个Java后台服务,在最后性能测试的时候发现,高并发条件下,该服务内存占用居高不下(跑10个小时以上能飙到4G)。这样奇高的内存占用肯定是无法接受的,所以需要排查到底是哪里出问题了。
环境介绍
- OS版本:
CentOS release 6.5 (Final)
- JDK版本:
1.7.0_79
排查过程以及踩的坑
1. 查看Linux系统上Java内存情况
a. 实时查看
在Google的过程中,发现jmap
这个JDK安装过程中自带的工具可以满足查看内存情况需求,所以就用命令行敲入jmap
,但是显示command not found
,说明jmap
工具在远程主机中不存在。在查看了Java环境后发现,由于远程主机只需要运行Java服务,所以只装了JRE,没有装JDK,所以自然没有jmap工具
。
接着就去下载了Linux版本的JDK,将安装包放在在/usr/java/
目录下并解压,然后配置环境变量
,完成JDK安装,用java -version
检查是否安装成功。(踩得坑
:由于我司提供256位数据加密服务,但由于出口限制,AES加密长度被限制为128位,这时候需要替换local_policy.jar
和US_export_policy.jar
两个jar包,否则无法正常加解密。路径在jdk里的jre目录下:/jre/lib/security
)
准备好JDK环境之后,命令行输入:jmap -histo <pid> | head -20
,就可以查看某个pid的java服务占用内存排名前20的类。注:该命令不适用子Windows系统里。
通过实时查看内存占用情况,发现占用内存最高的几个类大部分都是Netty的buffer相关类,那问题就可以确定存在于使用Netty框架时使用了buffer,但是没有调用buf.release()
去释放buffer。
b. 把heap文件dump下来分析
jmap
还有一个指令可以把整个内存情况转成文件形式保存下来:jmap -dump:format=b,file=filename.bin <pid>
- 从远程用
sz filename.bin
命令将文件下载到本地来,在用Eclipse
插件MAT(Memory Analyse Tool)
分析整个文件,MAT
插件需要下载安装,方法Google可以找到。
注:
在使用这个命令导出来的文件很大,但是最终MAT里分析出来的图表各个类占用的内存却很小,不知道是哪里出问题了,感到迷惑!
不过在这个之前,由于启动脚本里设置了如果发生OOM则会将hprof
文件dump出来,在其中一次测试中,.hprof
文件就产生了,下载到本地用MAT
进行分析后能看到以下结果:
这里面就能看到,ConcurrentHashMap
占用了大部分的内存,所以问题定位到使用该map的代码里。另外MAT工具还有很只管的饼图来呈现哪些类占用了很大内存。
2. 解决方案
a. 解决ConcurrentHashMap问题
在Review代码之后,发现ConcurrentHashMap会将整个http请求放入缓存,当时这么做的原因是,我们其实是一个通信转发的框架,所以需要把每个请求记录下来,等远端有返回后,可以找到对应的Channel将Response返回。而实际上,不需要缓存整个http请求,只要把请求进来时候的Channel
进行缓存即可,所以将ConcurrentHashMap的键值改成了<String, Channel>
,其中String
是UUID.randomUUID().toString()
产生的随机字符串。
b. 解决Netty的buffer没有被release问题
在Review代码之后,发现逻辑中有两个地方用到了ByteBuf
:
- Http请求进来时,会将
请求body
缓存到ByteBuf
里,这里是显式调用。解决:在返回Response之后将该buf释放掉
。 - HttpResponse中,也会先将
返回body
缓存到ByteBuf
里,这里是隐式调用,刚开始都没发现。解决:显式生成ByteBuf对象,采用response.content().writeBytes(buf)方法写入response,写完之后,调用buf.release()方法
。
注:关于ByteBuf
释放问题,如果Netty中的Handler
将buf
传递给其它类使用,则无需调用buf.release()
。