java jvm 内存溢出(OOM) 定位与优化

Java jvm内存溢出是指应用程序在运行的过程中,由于有不断的数据写入到内存,到导致内存不足,进程被系统内核杀死。所在在服务程序运行的时候,要观察一段时间的程序内存使用和分配情况。

故事原因

在一次游戏合服的操作之后,几个服的玩家被合并到同一个服,这个时候,玩家的数据量会猛增。突然就收到客服反应有些玩家登陆不进去了,一些在游戏中的玩家明显感觉到游戏卡顿。基于这些原因,首先就是查看系统的CPU和内存使用情况:

1. 使用top命令,查看cpu和内存的使用率
2. 使用ps -ef|grep 进程名,获取此程序的消息号

发现cpu使用率一直处于100%,内存也接近枯竭

使用命令:top -H -p 进程号

查看了一下此Java进程的所有线程,发现是gc线程一直在执行,好像进入了一个死循环:
gc线程占满CPU

内存RES使用了7G,而我们给JVM运行时设置的最大堆同存是6G,所以从这里可以确定是因为内存不足的原因导致的服务卡死或崩溃。

查找内存不足的原因

现在已经明确是因为内存不足导致的服务器异常,那么最主要的就是要查出是什么数据占用了大量的内存。
首先使用jmap命令查看当前内存的对象快照

 jmap -histo:live 进程号 > d.jmap

大于号是把查出的结果放到d.jmap文件中,文件名可以自己定义。
使用 less d.jmap命令打开统计文件,可以发现一些定义的对象实例数量很大:
大对象实例统计

从这些统计信息中可以获得哪些对象的实例数量比较大,为什么这么大这就和自己的业务有关系了,比如缓存内容太多,缓存清理速度没有增加速度快,再比如并高比较高,有大对象的序列化,总之是在同一时刻内存中出现了众多的大对象实例。根据分析之后,如果增加内存之后,还是不能解决问题,可以进行一波优化,然后再更新到线上服务。

观察内存的变化

当优化好之后,更新上线上,需要观察一些新的进程的变化。

jhsdb jmap --heap --pid 进程号
(对于jdk8之后的版本,不能再使用jmap -heap pid的命令了,需要使用上面的命令)。

可以查看到整个JVM内存的分配和使用情况

using thread-local object allocation.
Garbage-First (G1) GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 12884901888 (12288.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 2575302656 (2456.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 4194304 (4.0MB)

Heap Usage:
G1 Heap:      //整个JVM堆栈的使用情况
   regions  = 3072
   capacity = 12884901888 (12288.0MB)
   used     = 7882316288 (7517.16259765625MB)
   free     = 5002585600 (4770.83740234375MB)
   61.17482582728068% used
G1 Young Generation:   //年轻代的分配使用情况
Eden Space:
   regions  = 533
   capacity = 2541748224 (2424.0MB)
   used     = 2235564032 (2132.0MB)
   free     = 306184192 (292.0MB)
   87.95379537953795% used
Survivor Space:
   regions  = 39
   capacity = 163577856 (156.0MB)
   used     = 163577856 (156.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:    //老年代的使用情况
   regions  = 1308
   capacity = 10179575808 (9708.0MB)
   used     = 5483174400 (5229.16259765625MB)
   free     = 4696401408 (4478.83740234375MB)
   53.86446845546199% used

优化步骤一之内存分配

我们第一次是采取的增加内存的方式,把JVM的最大堆内存从6G增加到了12G,但是过段时间之后,程序又OOM崩溃了。程序重启之后,观察内存又快照被占用了。查看JVM的gc情况

jstat -gc 进程号 取样时间,例如:jstat -gc - 3333 5000 每5秒统计一个这个进程3333的gc情况。

发现很快执行了一次Full GC(FGC 的值为1),再观察JVM的内存使用情况,发现老年代内存分配比例比较小,因为我们业务有很多缓存,这些对象会长期留在内存中,最终被移到老年代之中。所以老年代应该被分配更多有内存。控制年轻代和老年代的分配比较的参数是NewRatio,(jvm启动中这样配置: -XX:NewRatio=n)JDK10中,如果不配置的话,默认是2,表示年经代和老年代的比值是1:2,年轻代占1/3,老年代占2/3。我们修改为了4,即 -XX:NewRatio=4。

业务缓存优化

这个就要根据大家的业务来自行查找和优化了,主要的思路有:

  1. 是否有缓存一直在加数据而没有移除策略
  2. 并发量是否过高,同时有大量的大对象序列化
  3. 是否有缓存穿透,大量数据缓存中没有,直接从数据库加载,比如我们新合服过来的玩家数据原来都没有缓存,都必须从数据库加载。而玩家的数据对象又非常大,需要反序列化为对象实例。
  4. 是否有大量线程创建

总结

这次OOM异常主要是合服之后玩家数据量猛增导致的,以前没有发现的原因是后期的功能未再做压力测试,玩家的数据随着游戏时间越来越长,量会越来越大。优化的方向主要有两个:一是JVM配置的优化,因为有大量的缓存,所以应该给老年代分配较多的内存,二是业务的优化,除了缓存之后,尽量减少大对象的创建,如果大部分业务必须要使用到这些大对象,可以把大对象中的这些数据拆分出来,变成小对象使用。
欢迎关注,获取更多文章


版权声明:本文为wgslucky原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。