一、声明
这里只是简单记录一下遇到的问题、以及中间的处理手段、最后的处理结果及个人分析结论,受限于个人知识水平和工作经历,其中的处理手段和分析结论可能是错的,如果有人因此被误导,我很抱歉,所以请读文章时多加分辨。
二、结论
先说结论,由于以前对JVM以及Java垃圾回收认识的不足,所以理所当然认为:Java程序在短时间内处理大量任务时会申请使用大量的内存空间,而等任务处理完毕(或者是请求的任务变少时),会有大量的内存被虚拟机回收回来,回收回来的内存由于短期内不再被使用,所以虚拟机会将内存归还给操作系统。
实际上以上认知是错误的,或者是由于对JVM的版本及其垃圾回收机制认识片面(也可以说是错误)。要记住几点:1、Java的垃圾回收简单地可以认为Java自己去管理已经从系统获得的内存,将不再被使用的对象占用的内存回收。2、Java将回收回来的大片内存,如果短期内没有被接着使用,不能理所当然地认为会被释放归还给操作系统,还不还受JVM版本(直接和GC版本有关)、JVM的参数配置影响。
所以,当有人跑来跟你说,你们的程序执行完任务后,内存使用率还一直处于高水位不下降,是不是你们的程序有bug,这时候至少你可以心理有个底:有时候程序执行完任务后就是不会释放内存,然后一直高内存占用运行。
三、起因
最近测试域对我们系统做压力性能测试,简单说就是模拟用户操作页面然后以多个线程并发调用我们的接口。当然压测是有通过指标的,比如压测时cpu使用率不能超过60%,内存使用率不能超过80%,单接口吞吐量不能低于30次每秒。在做压测的过程中遇上了很多问题,领导指派我来跟进压测,遇到问题也要负责结局。
先说一下我们的应用环境:我们的应用是SpringBoot应用,部署于两个docker节点上,每个节点上给应用分配的cpu只有2核,内存也只有2GB。
期初,测试结果不理想,内存使用率飙升到80%以上,cpu使用率也有60%,但吞吐量却不到30。
我之前也没有做过这样的工作,很多时候我们遇到问题都会先从自身找问题,比如认为可能是我们的程序写得不够合理,导致某些实例可能会频繁创建、而且会大量占用了内存,所以内存不够用。因此我们检查了程序,将那些需要经常从数据库读出大量数据构建出大数据结构对象(比如人员、机构树形对象)操作换成读取缓存。修改程序后发现,问题依旧存在,那就不是程序本身的问题了。
当然,这期间我还和测试域的同事沟通,学会了使用Jemter、以及他们的测试流程,还简化了压测的方式。接口的吞吐量上不来,其中一个原因是测试方式的问题,比如同时对多个接口测试,接口1测试调用了3000次,接口2调用了3000次,接口3调用了3次,接口4调用了3次,接口5调用了3次,但计算接口2吞吐量的时候完全不考虑接口1的调用(接口3、接口4、接口5调用次数太少可以忽略不计),这其实不太合理,因为接口1调用3000次而不是调用了3次,这会直接拉低借口的吞吐量。所以,测试改进接口调用方式。
询问了组内技术比较强的晓波同学。他分析是由于可用内存不够导致虚拟机需要大量GC来腾出内存来给新任务,同时GC也加剧了CPU使用率,所以吞吐量上不来。
最终,给每个节点分配8GB内存,同时设定-Xms2g -Xmx6g 。
结果晚上接到测试的通知:吞吐量达标了,但压测之后查看资源监控,发现程序占用内存为83%没有回落,测试不通过。
内存占用率(为什么会到83%呢?为什么之后不会降呢?)
以上就是本次面对的问题。
四、问题处理过程
1、没办法,硬着头皮上
工作好几年了,当没有实际地去分析和解决过相关问题,所以我面对以上问题也是没有半点头绪的。
但之前多少还是读过几页《深入虚拟机内幕》这本书,头脑中唯一知道的就是遇到和jvm、内存、垃圾回收的问题时要去使用top、ps、free、jps、jsata、map等命令去查看cpu、内存的使用状况,分析进程线程等等一大堆我没怎么听过、不熟悉、也不怎么会有的命令和分析技术。
我们的理解是,程序在空闲下来时就应该释放内存并归还给操作系统。
遇上这种事情必须自己上,感觉头很大。
2、艰难的尝试
没办法,那就花了几个小时再次去了解jstat、jmap命令。由于网路隔离,其他带图形化的分析工具也没法使用,只能用这几个命令。期初节点上没有安装jdk,也就有一个openjdk版本的jre,我只能再在上面安装一个openjdk8。由于有顾虑,认为可能再装一个jdk可能影响节点上需要使用jre运行的程序的执行,所以连安装jdk也是战战兢兢的,不过经过授权后放心地去干了。
当然中间输入的命令很多,做了很多事情,但最重要的是几个命令:
(1)top:查看java程序的进程号以及资源使用情况
我们的程序用了6.6GB空间,这个6.6/8约等于83%。
(2)ps -ef:查看进程号、启动参数等
(3)jmap:说实话,这个工具很有用,不过我虽然执行了,但不清楚怎么去分析;
(4)jstack:同上;
(5)jstat -gc 进程号:重点用这个来分析垃圾回收
比如以下就是我某次执行命令后copy出来的结果,然后自己做分析(分析过程很蹩脚,甚至之前连OC、OU什么含义我都不清楚只能再去查资料,大家忍受一下)
堆内存有6GB,但实际使用的有2.6GB。
3、陷入困境
结合以上这些数据结合我们的程序启动参数“-Xms2g -Xmx6g”,我们这里整理一下我们的已知的信息:
1、随着压力增大,我们的java程序的堆内存慢慢膨胀到6GB,也就是-Xmx6g全用上了。外加堆之外的600多MB内存,JVM共管理使用6.6GB内存。6.6GB约占整个节点内存的的83%。
2、压力测试后,JVM还是用着这6.6GB内存,但堆内存实际被使用的只有2.6GB。明明有空闲的内存,为什么你不释放???
而且,之后,我们做了很多压力测试,我们使用top命令来查看资源使用情况,使用的内存还是6.6GB。我想破了头也不知道它为什么没有任务了内存不降。
4、网上搜索
我尝试去联系以前的同事,问Java程序在任务压力减轻或者是没有任务时,是不是应该归还内存,结果他们都告诉我:“是的”。至于为什么我的程序没有归还,他们也不清楚,也帮不上什么忙。
同事、朋友帮不上,那就问问万能的百度吧
5、出现转机
我再百度上搜了很多内容,其中最关键条目是:“java压力测试后内存不释放”、“JVM最大堆内存不回落的原因”、“如何减少JVM中的已提交堆内存 ”、“JVM最大堆内存不下降”、“JVM需要归还多余的内存”、“请教为何JVM不把空闲内存归还给操作系统”、“查询jvm使用哪种垃圾回收器”、“UseParallelGC归还系统内存”
6、形成最初结论
我不知道碰了多少次壁,看了多少篇文章,最后意识到:JVM可能不会归还内存给操作系统。我参考了以下这些文章,得出结论,并向领导做了汇报:
https://www.jianshu.com/p/7893ff6c3324
https://exp.newsmth.net/topic/3c127cffb4c4f86e8b34a84cb295d8b1
https://www.itdaan.com/blog/2016/11/23/1efed2e6a4229e7892e586f6ce30ee8a.html
https://www.zhihu.com/question/357813017
https://www.pianshen.com/article/8937988907/
7、验证
领导问以上内容是谁说的,我没回复(都贴了连接了,那可定是是网上说的)。
他又问,有没有参数可以 让内存从操作系统层面降下来。
我说有参数,不过需要试试来验证是否有效。
之后我们就抽时间验证了。
在程序启动参数里加了“-XX:MaxHeapFreeRatio=50”后,程序在进行压测的时候,使用top命令查看,程序最多使用了2.8GB内存,之后再没上涨过。
领导也关注这个事,他认为是加了这个参数后,JVM判定达不到扩容条件,所以对内存没有涨,所以内存使用率不会上涨。
最后,又在程序启动参数里加入MinHeapFreeRatio=40 MaxHeapFreeRatio=70(之前只是加入MaxHeapFreeRatio=50),再次做压力测试,程序最多只用2.8G内存。
8、问题处理结果
为了保证压测通过,所以在压测的环境里,程序启动参数加了MinHeapFreeRatio=40 MaxHeapFreeRatio=70,保证压测是内存使用率满足通过要求(内存使用率2.8除以8,肯定是低于80%的)。
另外,由于我们设定参数后,由于堆内存的使用低于设定的水平,我猜测没有进行缩容(即JVM将内存归还给操作系统)。
虽然加这个参数没有使处于高位的内存水平下降(因为加了参数后,内存就没有上升到6GB的70%,就别提缩容了),但能通过压力测试了,这个参数的使用效果也和我们预想的不一致。
五、再提结论
最后,领导也认同了我的汇报内容:1、JVM默认不会释放归还暂时不用的内存给操作系统(这个和我们理所当然的认为不一样),当然有参数可以使之“归还”。2、不同的JVM这方面有差异;3、JVM管理的内存堆内空间的使用率虽然下降,但在外面的操作系统看来,你这Java程序还在占用从我申请走的那块内存,没有还给我(所以,部分监控软件看来你的Java程序就是过度消耗了内存不释放,你的程序一定有问题)。
六、尾声
晓波看到大周末的我们的工作群里还有讨论记录,就想写一篇文章记录一下,他比较擅长技术和分析。看了聊天内容后就知道为原因了,写了篇博文让我去看,我说:“其实这个问题给知道症结的人来解决3分钟就有结论了;而我却什么都不知道,也从来没有研究过相关的内容,导致浪费了一两天时间。早知道应该让你来处理这个问题了”。
其实还是我没有好好提高自己,很多进本的知识都没有掌握。