“螺蛳壳内做道场”——记一次线上内存调优(JVM、LINUX、内存过载)

        这是我的第一篇文章,权当记录一下自己在编程生涯中遇到的问题,同时也希望能帮到有需要的人。如果有问题欢迎纠正。

        本文一共分为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%,手机短信,邮箱一直发告警。

b14dae0bb24744abba8f3854e0b87413.png

用 free -h  命令查询内存情况:

6c7ebcec4c82496eb0c0e730cc5ce487.png

我们可以看到,free还剩118MB,可用内存不多了。虽然swap了2GB,但是只使用了380多MB,有些只能用内存,这里应该也优化不了了。

我再用 ps aux --sort -rss | head -10 命令查看linux的前十大占用内存的应用:

85f04d4f28b6484aba714b885d93c997.png

可以看到,前4个除了jdk包之外都是我的java进程,这样看来内存优化的方向转向了JVM优化。

我们可以在上面的图里面找到java占用的进程号,也能通过ps -aux | grep java来查看java进程相关的进程号。

我以占用最高的两个模块举例:后端(900多MB)和网关(近800MB)。

用jinfo -flags  <pid>  来查看他们的堆内存,发现:

后端:

ee155c6d7e3b468ca8f2fd3f88b9c069.png

网关:

83a6ec33ee654879a050ad6a114aafe4.png

它们的初始内存和最大内存分别都是500MB、900MB。再详细看里面jvm内不同代内存占用:

jmap -heap <pid>

后端模块jvm配置:

8dbe5e0f1611472284cc198da0e3f198.png

后端模块jvm各代内存占用情况:

21607b7b878846ada283f82ddfa789b6.png

网关模块jvm配置:

c6efd49066c1403e9e0566ce4878f99e.png

网关模块jvm各代内存占用情况:

faaf21ecd71943edbb40f95f5fffbb2c.png

好家伙,大家有没有发现,里面存在几个问题:

1.首先我的jvm内存实际占用内存没那么高,为什么java进程占用了900多MB和800MB

2.新生代的Survival区的内存比例和Eden的内存比例严重失调。

这时我想到了应该和垃圾回收器有关。

我的进程和linux环境都是jdk8,jdk8默认的垃圾回收器是Paraller。

java -XX:+PrintCommandLineFlags -version  如下所示:

66c61b51909b410098dc92003680a63e.png

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:

7bf8de4e0e6a4e17b0a32f2b290b8af9.png

在发生full gc 前的内存占用情况:

a32469c1b37a4ce1bd0ab55900360b67.png

我们可以用 jcmd <pid> GC.run来强制执行full gc,并发现:

c138cfc8611446c7afddbbbafa5d00ec.png

内存占比果然下来了,jvm归还了部分空间给操作系统。

此时总的linux内存情况为:

264c5e506e4149d7b500527a752b95f1.png

 

这里附上不同垃圾收集器适用的场景:

c8f711ede31944afa4c88924a6e662b6.jpeg

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值