这是我的第一篇文章,权当记录一下自己在编程生涯中遇到的问题,同时也希望能帮到有需要的人。如果有问题欢迎纠正。
本文一共分为4章节,分别是项目背景、内存过载问题排查、当前存在的隐患以及优化策略。话不多说,马上开始。
一、项目背景
(想看问题可直接可直接跳过本章节...)
本项目是我自己的个人项目,微服务架构,一共5个模块:后端模块、网关模块、接口处理模块、common模块、sdk模块,主要是前三个模块工作。服务器用的是阿里云的服务器,4GB内存。主要涉及到的进程有:三个java程序、mysql、redis、nacos、rabbitt mq、docker、nginx等。(麻雀虽小五脏具存哈哈)
由于本人是学生,也买不起太高的服务器配置,所以在内存上也是要尽可能做节约使用。同时希望能给到那些预算有限下,做个人网站或项目的人们一些参考和借鉴。如果有不对或漏的地方也十分欢迎在评论区友好讨论。
一开始上线项目,我买的是阿里云99/年的学生套餐,2GB内存,服务还没起完就直接撑爆内存,含泪又升级到了4GB。因为当初也是经验有限,不懂得JVM调优,即便4GB的内存其实也是很吃紧。我把rabbit mq换成docker容器来启动,同时用swap了2GB的虚拟内存。勉强内存占用定格在88%。然而随着这大半年的发展,内存占用一直变高,知道从88%来到90%,最后来到94%,我实在忍无可忍了,硬逼着自己去做优化,顺便复习一下jvm的知识(bushi),这便是本文章来由。
二、内存过载问题排查
这是linux的内存情况,一直没低过90%,手机短信,邮箱一直发告警。
用 free -h 命令查询内存情况:
我们可以看到,free还剩118MB,可用内存不多了。虽然swap了2GB,但是只使用了380多MB,有些只能用内存,这里应该也优化不了了。
我再用 ps aux --sort -rss | head -10 命令查看linux的前十大占用内存的应用:
可以看到,前4个除了jdk包之外都是我的java进程,这样看来内存优化的方向转向了JVM优化。
我们可以在上面的图里面找到java占用的进程号,也能通过ps -aux | grep java来查看java进程相关的进程号。
我以占用最高的两个模块举例:后端(900多MB)和网关(近800MB)。
用jinfo -flags <pid> 来查看他们的堆内存,发现:
后端:
网关:
它们的初始内存和最大内存分别都是500MB、900MB。再详细看里面jvm内不同代内存占用:
jmap -heap <pid>
后端模块jvm配置:
后端模块jvm各代内存占用情况:
网关模块jvm配置:
网关模块jvm各代内存占用情况:
好家伙,大家有没有发现,里面存在几个问题:
1.首先我的jvm内存实际占用内存没那么高,为什么java进程占用了900多MB和800MB
2.新生代的Survival区的内存比例和Eden的内存比例严重失调。
这时我想到了应该和垃圾回收器有关。
我的进程和linux环境都是jdk8,jdk8默认的垃圾回收器是Paraller。
java -XX:+PrintCommandLineFlags -version 如下所示:
Paraller垃圾回收器是以吞吐量为目标的垃圾回收器,这里的吞吐量是指总时间与垃圾回收时间的比例。这个比例越高,证明垃圾回收占整个程序运行的比例越小。Paraller在这个目标下,会调高Eden区的内存,减少GC次数,以增大吞吐量(一般情况下是这样,但是垃圾回收器会有自己的策略根据实际情况进行动态调整)。结合我这个程序,其实流量很低,没什么人访问,而且涉及到的调用链路不复杂,不会产生太多大对象(这里应该jmap dump一下,但是我怕把我程序搞崩了,所以没弄这些操作),在这个情况下,jvm会慢慢调高他的Eden区占比,直到我们Eden区和Survival区的比例来到夸张的100:1(初始是8:2)。
此外,Paraller还有一个问题,就是它不会归还JVM空闲的内存给操作系统,因此导致jvm堆内存虽然占用的空间不多,但是整个进程占用了过多的内存空间。
因此后面着手从垃圾回收器的角度来优化jvm。
三、当前存在的隐患
我们看到,上面主要存在的问题为两个:
1.jvm没有归还空闲内存给操作系统
2.survival区占比过小
第一个问题,当然是对我们系统有影响,长此以往会压缩其他进程的空间,并且对宝贵的内存空间造成浪费。还有可能造成系统OOM的风险。
对于第二个问题,由于是垃圾回收器的原因,动态调整了eden和survival区的比例。风险是当某时刻大量流量进来时,会创建很多对象,并很快塞满eden-->触发young gc。而survival不足的话会导致大量的对象进入到老年区,并塞满老年区,引发full gc。
四、优化策略
垃圾收集器的选择有两个需要考量的地方:吞吐量还是响应时间
吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
响应时间 = 平均每次的GC的耗时
通常,吞吐优先还是响应优先这个在JVM中是一个两难之选。
堆内存增大,gc一次能处理的数量变大,吞吐量大;但是gc一次的时间会变长,导致后面排队的线程等待时间变长;相反,如果堆内存小,gc一次时间短,排队等待的线程等待时间变短,延迟减少,但一次请求的数量变小(并不绝对符合)。
因此,我决定将paraller的垃圾回收器改成G1。G1垃圾回收器是以响应时间为目标的垃圾回收器,且当发生gc时,jvm会根据实际情况归还部分内存空间给操作系统。Paraller垃圾收集器适合在计算比较多的场景而不是交互比较多,但实际我的程序并没有重计算的场景。
这是修改了垃圾回收器的jvm:
在发生full gc 前的内存占用情况:
我们可以用 jcmd <pid> GC.run来强制执行full gc,并发现:
内存占比果然下来了,jvm归还了部分空间给操作系统。
此时总的linux内存情况为:
这里附上不同垃圾收集器适用的场景: