运维:你们 JAVA 服务内存占用太高,还只增不减!告警了,快来接锅
===================
先点赞再看,养成好习惯
某天,运维老哥突然找我:“你们的某 JAVA 服务内存占用太高,告警了!GC 后也没释放,内存只增不减,是不是内存泄漏了!”
然后我赶紧看了下监控,一切正常,距离上次发版好几天了,FULL GC 一次没有,YoungGC,十分钟一次,堆空闲也很充足。
运维:“你们这个服务现在堆内存 used 才 800M,但这个 JAVA 进程已经占了 6G 内存了,是不是你们程序出啥内存泄露的 bug 了!”
我想都没想,直接回了一句:“不可能,我们服务非常稳定,不会有这种问题!”
不过说完之后,内心还是自我质疑了一下:会不会真有什么bug?难道是堆外泄露?线程没销毁?导致内存泄露了???
然后我很“镇定”的补了一句:“我先上服务器看看啥情况”,被打脸可就不好了,还是不要装太满的好……
迅速上登上服务器又仔细的查看了各种指标,Heap/GC/Thread/Process 之类的,发现一切正常,并没有什么“泄漏”的迹象。
和运维的“沟通”
我们这个服务很正常啊,各个指标都ok,什么内存只增不减,在哪呢
运维:你看你们这个 JAVA 服务,堆现在 used 才 400MB,但这个进程现在内存占用都 6G 了,还说没问题?肯定是内存泄露了,锅接好,赶紧回去查问题吧
然后我指着监控信息,让运维看:“大哥你看这监控历史,堆内存是达到过 6G 的,只是后面 GC 了,没问题啊!”
运维:“回收了你这内存也没释放啊,你看这个进程 Res 还是 6G,肯定有问题啊”
我心想这运维怕不是个der,JVM GC 回收和进程内存又不是一回事,不过还是和得他解释一下,不然一直baba个没完
“JVM 的垃圾回收,只是一个逻辑上的回收,回收的只是 JVM 申请的那一块逻辑堆区域,将数据标记为空闲之类的操作,不是调用 free 将内存归还给操作系统”
运维顿了两秒后,突然脸色一转,开始笑起来:“咳咳,我可能没注意这个。你再给我讲讲 JVM 的这个内存管理/回收和进程上内存的关系呗”
虽然我内心是拒绝的,但得罪谁也不能得罪运维啊,想想还是给大哥解释解释,“增进下感情”
操作系统 与 JVM的内存分配
JVM 的自动内存管理,其实只是先向操作系统申请了一大块内存,然后自己在这块已申请的内存区域中进行“自动内存管理”。JAVA 中的对象在创建前,会先从这块申请的一大块内存中划分出一部分来给这个对象使用,在 GC 时也只是这个对象所处的内存区域数据清空,标记为空闲而已
运维:“原来是这样,那按你的意思,JVM 就不会将 GC 回收后的空闲内存还给操作系统了吗?”
为什么不把内存归还给操作系统?
JVM 还是会归还内存给操作系统的,只是因为这个代价比较大,所以不会轻易进行。而且不同垃圾回收器 的内存分配算法不同,归还内存的代价也不同。
比如在清除算法(sweep)中,是通过空闲链表(free-list)算法来分配内存的。简单的说就是将已申请的大块内存区域分为 N 个小区域,将这些区域同链表的结构组织起来,就像这样:
每个 data 区域可以容纳 N 个对象,那么当一次 GC 后,某些对象会被回收,可是此时这个 data 区域中还有其他存活的对象,如果想将整个 data 区域释放那是肯定不行的。
所以这个归还内存给操作系统的操作并没有那么简单,执行起来代价过高,JVM 自然不会在每次 GC 后都进行内存的归还。
怎么归还?
虽然代价高,但 JVM 还是提供了这个归还内存的功能。JVM 提供了-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio 两个参数,用于配置这个归还策略。
MinHeapFreeRatio 代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为 Xmx
MaxHeapFreeRatio 代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为Xms
不过虽然有这个归还的功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。
但是但是但是,经过实测,这个归还内存的机制,在不同的垃圾回收器,甚至不同的 JDK 版本中还不一样!
不同版本&垃圾回收器下的表现不同
下面是我之前跑过的测试结果:
public static void main(String[] args) throws IOException, InterruptedException {
List<Object> dataList = new ArrayList<>();
for (int i = 0; i < 25; i++) {
byte[] data = createData(1024 * 1024 * 40);// 40 MB
dataList.add(data);
}
Thread.sleep(10000);
dataList = null; // 待会 GC 直接回收
for (int i = 0; i < 100; i++) {
// 测试多次 GC
System.gc();
Thread.sleep(1000);
}
System.in.read();
}
public static byte[] createData(int size){
byte[] data = new byte[size];
for (int i = 0; i < size; i++) {
data[i] = Byte.MAX_VALUE;
}
return data;
}
测试结果刷新了我的认知。,MaxHeapFreeRatio 这个参数好像并没有什么用,无论我是配置40,还是配置90,回收的比例都有和实际的结果都有很大差距。
但是文档中,可不是这么说的……
而且 ZGC 的结果也是挺意外的,JEP 351 提到了 ZGC 会将未使用的内存释放,但测试结果里并没有。
除了以上测试结果,stackoverflow 上还有一些其他的说法,我就没有再一一测试了
JAVA 9 后-XX:-ShrinkHeapInSteps参数,可以让 JVM 已非线性递增的方式归还内存
JAVA 12 后的 G1,再应用空闲时,可以自动的归还内存
所以,官方文档的说法,也只能当作一个参考,JVM 并没有过多的透露这个实现细节。
不过这个是否归还的机制,除了这位“热情”的运维老哥,一般人也不太会去关心,巴不得 JVM 多用点内存,少 GC 几回……
而且别说空闲自动归还了,我们希望的是一启动就分配个最大内存,避免它运行中扩容影响服务;所以一般 JAVA 程序还会将 Xms和Xmx配置为相等的大小,避免这个扩容的操作。
听到这里,运维老哥若有所思的说到:“那是不是只要我把 Xms 和 Xmx 配置成一样的大小,这个 JAVA 进程一启动就会占用这个大小的内存呢?”
我接着答到:“不会的,哪怕你 Xms6G,启动也只会占用实际写入的内存,大概率达不到 6G,这里还涉及一个操作系统内存分配的小知识”
Xms6G,为什么启动之后 used 才 200M?
进程在申请内存时,并不是直接分配物理内存的,而是分配一块虚拟空间,到真正堆这块虚拟空间写入数据时才会通过缺页异常(Page Fault)处理机制分配物理内存,也就是我们看到的进程 Res 指标。
可以简单的认为操作系统的内存分配是“惰性”的,分配并不会发生实际的占用,有数据写入时才会发生内存占用,影响 Res。
所以,哪怕配置了Xms6G,启动后也不会直接占用 6G 内存,实际占用的内存取决于你有没有往这 6G 内存区域中写数据的。
运维:“卧槽,还有惰性分配这种东西!长知识了”
我:“这下明白了吧,这个内存情况是正常的,我们的服务一点问题都没有”
运维:“🐂🍺,是我理解错了,你们这个服务没啥问题”
我:“嗯呐,没事那我先去忙(摸鱼)了”
总结
对于大多数服务端场景来说,并不需要JVM 这个手动释放内存的操作。至于 JVM 是否归还内存给操作系统这个问题,我们也并不关心。而且基于上面那个测试结果,不同 JAVA 版本,不同垃圾回收器版本区别这么大,更是没必要去深究了。
综上,JVM 虽然可以释放空闲内存给操作系统,但是不一定会释放,在不同 JAVA 版本,不同垃圾回收器版本下表现不同,知道有这个机制就行。
参考
https://docs.oracle.com/javase/10/gctuning/factors-affecting-garbage-collection-performance.htm#JSGCT-GUID-B0BFEFCB-F045-4105-BFA4-C97DE81DAC5B
https://stackoverflow.com/questions/30458195/does-gc-release-back-memory-to-os
《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》 - 周志明 著
————————————————
版权声明:本文为CSDN博主「空无c」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq826654664jx/article/details/117222044
转载自:https://blog.csdn.net/ancoa/article/details/125354061
=============================
一、声明
这里只是简单记录一下遇到的问题、以及中间的处理手段、最后的处理结果及个人分析结论,受限于个人知识水平和工作经历,其中的处理手段和分析结论可能是错的,如果有人因此被误导,我很抱歉,所以请读文章时多加分辨。
二、结论
先说结论,由于以前对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%没有回落,测试不通过。
要保证测试通过,需要保证吞吐量(这个已经达标)、cpu使用率(这个之后再验证吧,jemeter将线程组的线程数降低一点就可以降低cpu使用率,当然吞吐量也会降但能达标)、内存占用率(为什么会到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://blog.csdn.net/qq826654664jx/article/details/117222044
https://blog.csdn.net/qq_40378034/article/details/110677269
https://bbs.csdn.net/topics/392052589
https://www.itdaan.com/blog/2016/11/23/1efed2e6a4229e7892e586f6ce30ee8a.html
https://blog.csdn.net/qq_39630314/article/details/101345122
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程序就是过度消耗了内存不释放,你的程序一定有问题)。
————————————————
版权声明:本文为CSDN博主「ancoa」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ancoa/article/details/125354061