本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
一、发现问题
这段时间持续收到项目预发环境的告警,提示内存占用过高,最高将近 90%。在预发环境下,内存持续走高的问题,已经持续了一段时间。尤其在周末无人发布版本,即没有重启应用程序的情况下,内存会持续堆积,并伴随频繁触发 GC 的情况。
上图中,比较明显的看到 1.14 - 1.17 期间,Eden Space、Survivor Space、Tenured Gen 三代内存占用持续走高,GC 的频率异常频繁,怀疑存在以下问题:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
- 集合类中有对对象的引用,使用完后未清空,使得
jvm不能回收。 - 代码中存在死循环或循环产生过多重复的对象实体。
- 使用的第三方软件中的
BUG。 - 启动参数内存值设定的过小。
- 服务器 /
pod本身配置问题。
二、问题追踪
1. 疑似点 - buff/cache 缓存
最初直接使用 free -h 命令查看服务内存的使用情况,发现其中 buff/cache 这块会占用大量空间,迅速从 7.8 -> 9.7 -> 10.1 G,因为猜测这个就是内存持续走高的原因。
buff/cache 是 linux 系统为了提升响应速度,用来将硬盘中数据缓存入内存。
经过一段时间(10 - 16 点)的观察,内存只是暂时被占用,该部分内存在应用程序需要时,可提供可用的内存空间,监控下应用程序内存仍在走高,但 buff/cache 并未有过大波动。而且后来发现在 pod 中,free -h 获取的是宿主机的内存指标,所以基本上排除了这个疑似项的原因。
2. 疑似点 - JVM 启动参数配置过小,导致频繁 GC
通过 ps aux 命令查看进程,确认我们要找的 java 进程,当然在当前预发的 pod 中我们只有一个 java 进程,ID 为 1
$ ps aux | grep java
admin 1 55.4 2.8 4609772 452828 ? Ssl 18:28 1:49 java -jar xxx-1.0.0-SNAPSHOT.jar
admin 301 0.0 0.0 112716 2324 pts/0 S+ 18:31 0:00 grep --color=auto java
通过 jmap 命令,查看下这个进程的 jvm 配置,堆栈信息、分代信息等
$ jmap -heap 1
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 805306368 (768.0MB)
NewSize = 16777216 (16.0MB)
MaxNewSize = 268435456 (256.0MB)
OldSize = 33554432 (32.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 89063424 (84.9375MB)
used = 4388256 (4.184967041015625MB)
free = 84675168 (80.75253295898438MB)
4.927113514072848% used
Eden Space:
capacity = 79167488 (75.5MB)
used = 1598464 (1.5244140625MB)
free = 77569024 (73.9755859375MB)
2.019091473509934% used
From Space:
capacity = 9895936 (9.4375MB)
used = 2789792 (2.660552978515625MB)
free = 7106144 (6.776947021484375MB)
28.191289838576157% used
To Space:
capacity = 9895936 (9.4375MB)
used = 0 (0.0MB)
free = 9895936 (9.4375MB)
0.0% used
tenured generation:
capacity = 197283840 (188.14453125MB)
used = 156359928 (149.11644744873047MB)
free = 40923912 (39.02808380126953MB)
79.25632834397383% used
51785 interned Strings occupying 5678744 bytes.
通过上面的参数配置,结合一些最佳实践文章,所配置的参数基本符合 jvm 需求,没发现特别特殊的参数异常或过小的情况。这块比较关注的几个点:
- 各代大小,过小会导致频繁的
GC情况 - 堆大小,过小容易引起内存溢出情况
- 老年代与新生代直接的比例,有待验证
通过对上面两个非代码层面的问题排查,基本上可以排查,以上两个原因不是内存持续增高的 主要 原因,还是要集中目光在代码层面,内存溢出的嫌疑很大。
三、定位问题
因为预发 jvm 启动中,没有加入 GC 相关的日志参数,所以这块没有结合到具体 GC 日志。而是通过监控下 GC 之后,三代的内存确实有大幅回收,但仍是呈持续走高的情况,断定肯定存在对象被引用一直未被释放或来不及释放的情况出现。
由于在预发服务器 jvm 不好连接调试,所以就是用主动 dump 内存快照的方式来定位问题,其实查阅文档过程中,发现 java 应用启动时有一个参数可以在内存溢出时候自动输出快照。但是 dump 持续时间很长,会导致应用程序的 OS 的卡顿,仔细想想这种方式也确实不适合线上来用,还是老老实实的去手动 dump 吧。这块除了 dump 之外,还可以直接通过查看堆实例对象分布来进行初步排查。
$ jmap -histo:live 1 | head -n 20
num #instances #bytes class name
----------------------------------------------
1: 179270 17393680 [C
2: 12491 7040816 [B
3: 201608 6451456 java.util.concurrent.ConcurrentHashMap$Node
4: 101056 4850688 org.aspectj.weaver.reflect.ShadowMatchImpl
5: 49516 4357408 java.lang.reflect.Method
6: 177978 4271472 java.lang.String
7: 101056 3233792 org.aspectj.weaver.patterns.ExposedState
8: 31956 2624896 [Ljava.lang.Object;
9: 23088 2563232 java.lang.Class
10: 994 2136768 [Ljava.util.concurrent.ConcurrentHashMap$Node;
11: 17034 1795496 [I
12: 46570 1490240 java.util.HashMap$Node
13: 13443 1270632 [Ljava.util.HashMap$Node;
14: 30711 1228440 java.util.LinkedHashMap$Entry
15: 14387 805672 java.util.LinkedHashMap
16: 49234 787744 java.lang.Object
17: 280 738040 [J
上表中的一些很大的对象都没有直观的看出与哪个对象相关,所以还是采用 dump 的方式去快照。在 pod 中执行,执行 jmap -dump 命令。
$ jmap -dump:format=b,file=jmap-dump-20220117-01.hporf 1
OK ~,当前我们已经拿到了快照的内容,我们通过 MAT 内存分析工具来展开快照内容。很快定位到了几个问题:
- 定时巡检任务中有一个较大的类,该对象被多次重复创建且进行了
Json转换,巡检过程大约会持续10分钟左右,该对象造成很大的内存占用。 fastjson的包,一直都会持续占用内存,且有持续慢增长的趋势,下面图中,占用了10M左右的空间。
因为上面的 dump 都是手动进行触发的,所以有可能不是内存占用最高峰或者触发 GC 之后的情况,所以多打几个包来进行对比,可以先手动 GC 之后,再进行 dump。
五、效果
根据监控下 GC 频率和多代内存占用来看,应该是解决了问题。